@kenkaiiii/ggcoder 5.74.2 → 5.74.3

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.
@@ -27,6 +27,7 @@ import { setStreamDiagnostic } from "@kenkaiiii/gg-agent";
27
27
  import { AgentSession } from "./core/agent-session.js";
28
28
  import { RunLifecycle } from "./core/run-lifecycle.js";
29
29
  import { RunClaim } from "./core/run-claim.js";
30
+ import { shouldOfferPendingPlan } from "./core/plan-handoff.js";
30
31
  import { CHAT_AGENT_IDS, chatAgentSessionsDir, createChatAgent, parseChatAgentId, switchChatAgent, } from "./chat-agents/index.js";
31
32
  import { createMotionAgentSession } from "./motion-agent/motion-agent.js";
32
33
  import { buildJiwaTools, JiwaStore } from "./chat-agents/jiwa.js";
@@ -1435,6 +1436,8 @@ async function createSession(deps, opts) {
1435
1436
  session = new AgentSession({
1436
1437
  ...baseSessionOptions,
1437
1438
  additionalTools: [askUserTool],
1439
+ // A run must not end mid-plan with nothing submitted: nobody could review it.
1440
+ planSubmissionGate: true,
1438
1441
  // Plan mode belongs only to the coding agent.
1439
1442
  onEnterPlan: async (reason) => {
1440
1443
  deactivateApprovedPlan();
@@ -2101,6 +2104,47 @@ async function createSession(deps, opts) {
2101
2104
  pendingPlanContent = "";
2102
2105
  planGeneration++;
2103
2106
  }
2107
+ // Plan feedback (Ken's or the user's) is revised back in read-only plan
2108
+ // mode. exit_plan leaves plan mode, so without this a "revise the plan" run
2109
+ // had full write access and could start implementing before any approval.
2110
+ async function reenterPlanModeForRevision() {
2111
+ if (session.getPlanMode())
2112
+ return;
2113
+ deactivateApprovedPlan();
2114
+ await session.setPlanMode(true);
2115
+ broadcast("plan_mode", { active: true });
2116
+ log("INFO", "app-sidecar", "plan mode re-entered for revision");
2117
+ }
2118
+ // Plan generation last handed to the human review box (-1: none yet).
2119
+ let offeredPlanGeneration = -1;
2120
+ // The plan the human review box should show right now, or null. Only once
2121
+ // everything has settled — see shouldOfferPendingPlan.
2122
+ function pendingPlanForHuman(offeredGeneration) {
2123
+ const offer = shouldOfferPendingPlan({
2124
+ planPath: pendingPlanPath,
2125
+ generation: planGeneration,
2126
+ offeredGeneration,
2127
+ running,
2128
+ starting: runClaim.active,
2129
+ autopilotActive,
2130
+ queued: session.getQueuedCount(),
2131
+ });
2132
+ return offer && pendingPlanPath !== null
2133
+ ? { planPath: pendingPlanPath, content: pendingPlanContent }
2134
+ : null;
2135
+ }
2136
+ // Open the Accept / Feedback / Reject box once nothing else will act on the
2137
+ // submitted plan. Called at every settle point (turn end, queue drain, task
2138
+ // run end). Without it, a plan Ken didn't approve — handed back, review
2139
+ // failed, capped, cancelled, or skipped — sat pending with no way to act.
2140
+ function offerPendingPlan() {
2141
+ const plan = pendingPlanForHuman(offeredPlanGeneration);
2142
+ if (!plan)
2143
+ return;
2144
+ offeredPlanGeneration = planGeneration;
2145
+ log("INFO", "app-sidecar", "plan handed to user for review", { planPath: plan.planPath });
2146
+ broadcast("plan_review", plan);
2147
+ }
2104
2148
  // Workflow (prompt-template) commands: built-in + the project's custom
2105
2149
  // `.gg/commands/*.md`. Used to gate autopilot off command turns and to label
2106
2150
  // expanded templates in Ken's digests. Loaded fresh so a newly added custom
@@ -2523,7 +2567,12 @@ async function createSession(deps, opts) {
2523
2567
  // control flow lives in driveAutopilotCycle (core/autopilot-cycle.ts) so
2524
2568
  // every exit path is unit-tested; this only wires the real dependencies.
2525
2569
  async function runAutopilotCycle(originalRequest) {
2526
- if (!autopilot || autopilotCancelled || session.getVerificationProblem())
2570
+ if (!autopilot || autopilotCancelled)
2571
+ return;
2572
+ // Unverified work skips a work review silently. A pending plan still enters
2573
+ // the cycle so driveAutopilotCycle says WHY Ken stepped aside
2574
+ // (autopilot_human) before the plan is handed to the user.
2575
+ if (pendingPlanPath === null && session.getVerificationProblem())
2527
2576
  return;
2528
2577
  const generation = runLifecycle.begin(abortOwnedWork).generation;
2529
2578
  pendingCancelDrain = null;
@@ -2611,7 +2660,11 @@ async function createSession(deps, opts) {
2611
2660
  },
2612
2661
  // Autopilot-injected run: GG Coder receives the framed prompt (no human
2613
2662
  // is watching this turn) while run_start keeps the clean label.
2614
- runPrompt: (body) => runAgent(body, () => session.prompt(frameAutopilotInjection(body), AUTOMATION_PROVENANCE)),
2663
+ runPrompt: (body, opts) => runAgent(body, async () => {
2664
+ if (opts?.planRevision)
2665
+ await reenterPlanModeForRevision();
2666
+ await session.prompt(frameAutopilotInjection(body), AUTOMATION_PROVENANCE);
2667
+ }),
2615
2668
  emit: (event) => {
2616
2669
  // Persist the terminal verdict marker so a resumed session renders the
2617
2670
  // same Ken bubble the live run showed instead of dropping it or
@@ -2677,8 +2730,10 @@ async function createSession(deps, opts) {
2677
2730
  if (!next.text.trim() && next.attachments.length === 0)
2678
2731
  continue;
2679
2732
  // A queued message draining as a fresh turn supersedes any pending
2680
- // plan, exactly like a direct POST /prompt turn.
2733
+ // plan and clears a stale Stop, exactly like a direct POST /prompt
2734
+ // turn — otherwise an earlier Stop silently disables Ken for this turn.
2681
2735
  clearPendingPlan();
2736
+ autopilotCancelled = false;
2682
2737
  const workflowCommand = next.attachments.length === 0 &&
2683
2738
  isWorkflowCommandText(next.text, await loadWorkflowCommandSpecs());
2684
2739
  const assistantsBefore = countAssistantMessages(session.getMessages());
@@ -2722,6 +2777,7 @@ async function createSession(deps, opts) {
2722
2777
  }
2723
2778
  finally {
2724
2779
  drainingStrandedQueue = false;
2780
+ offerPendingPlan();
2725
2781
  }
2726
2782
  }
2727
2783
  // ── Task runner (project task list → sessions) ──────────────
@@ -2768,6 +2824,9 @@ async function createSession(deps, opts) {
2768
2824
  }
2769
2825
  taskRunAll = false;
2770
2826
  broadcast("tasks_run_done", {});
2827
+ // Task runs never start an autopilot cycle; a plan a task submitted goes
2828
+ // straight to the user.
2829
+ offerPendingPlan();
2771
2830
  }
2772
2831
  // ── Provider auth (login) bridge ───────────────────────────
2773
2832
  // OAuth login functions are interactive (open a URL, sometimes prompt for a
@@ -3003,6 +3062,9 @@ async function createSession(deps, opts) {
3003
3062
  chatAgent,
3004
3063
  running,
3005
3064
  reviewPending: autopilotActive,
3065
+ // A reconnecting window (reload/reopen) re-opens the review box for
3066
+ // a plan still waiting on the user, regardless of earlier offers.
3067
+ pendingPlan: pendingPlanForHuman(-1),
3006
3068
  runState: runLifecycle.state,
3007
3069
  thinkingLevel: session.getThinkingLevel() ?? null,
3008
3070
  supportedThinkingLevels: getSupportedThinkingLevels(st.provider, st.model),
@@ -3762,6 +3824,9 @@ async function createSession(deps, opts) {
3762
3824
  const assistantsBefore = countAssistantMessages(session.getMessages());
3763
3825
  const messagesBefore = session.getMessages().length;
3764
3826
  await runAgent(text, async () => {
3827
+ // The review box's Feedback: revise read-only, like Ken's feedback.
3828
+ if (meta?.planRevision === true)
3829
+ await reenterPlanModeForRevision();
3765
3830
  if (attachments.length > 0) {
3766
3831
  // Persist each attachment under .gg/uploads so files are inspectable
3767
3832
  // by the agent's tools, then prompt with the media as native blocks.
@@ -3816,6 +3881,8 @@ async function createSession(deps, opts) {
3816
3881
  if (claimedStart) {
3817
3882
  scheduledRunActive = false;
3818
3883
  runClaim.release();
3884
+ // The claim held the plan back until the turn fully settled.
3885
+ offerPendingPlan();
3819
3886
  }
3820
3887
  });
3821
3888
  return;
@@ -4469,7 +4536,7 @@ async function createSession(deps, opts) {
4469
4536
  json(res, 400, { error: "invalid JSON body" });
4470
4537
  return;
4471
4538
  }
4472
- if (running) {
4539
+ if (running || runClaim.active) {
4473
4540
  json(res, 409, { error: "cannot accept a plan while the agent is running" });
4474
4541
  return;
4475
4542
  }