Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
0620d05
Strip Pi annotate command to thin CLI wrapper
backnotprop May 27, 2026
d9d77d1
Trim Pi vendor script, inline types, remove unused dependencies
backnotprop May 27, 2026
46af6b7
Fix inlined DiffType and VcsSelection to use actual union types
backnotprop May 27, 2026
484db1a
Fix: pass raw args to binary, use result metadata for feedback
backnotprop May 27, 2026
462bd32
Move feedback prompt generation to the server
backnotprop May 27, 2026
dd94184
Document fact: server owns feedback prompt generation
backnotprop May 27, 2026
58bf765
Fix CLI review and OpenCode annotate-last to pipe server prompt
backnotprop May 27, 2026
de61ff7
CLI as dumb pipe: move plan denial prompts to server, remove Jina config
backnotprop May 27, 2026
1f81b1c
Complete CLI dumb-pipe: move improve-context to daemon, remove dead code
backnotprop May 27, 2026
c8717d5
Final vendor trim: 14 files → 9, eliminate arg parsers and agents bloat
backnotprop May 27, 2026
c851699
Fix 4 review findings: improve-context crash, annotate prompt piping,…
backnotprop May 27, 2026
2b682f4
Anchor annotate-last feedback to original message via server-composed…
backnotprop May 27, 2026
69892b4
Fix improve-context to use discoverDaemon instead of ensureDaemonClient
backnotprop May 27, 2026
b500ecc
Fix improve-context to start daemon if needed, fail silently if it can't
backnotprop May 27, 2026
102e618
Thread bestEffort through daemon state cleanup to prevent process.exit
backnotprop May 28, 2026
69e12db
Bump plugin protocol version to 2 for server-composed prompts
backnotprop May 28, 2026
2691439
Clean up dead code: deduplicate types, remove unused functions
backnotprop May 28, 2026
c7fa928
Preserve raw feedback in --json and --hook annotate output
backnotprop May 28, 2026
41a0e13
Thread planFilePath through daemon to restore Gemini plan-file guidance
backnotprop May 28, 2026
ff07f5c
Match Pi's inlined DiffType to canonical review-core definition
backnotprop May 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 34 additions & 62 deletions apps/hook/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,31 +47,20 @@
* PLANNOTATOR_PORT - Fixed port to use (default: random locally, 19432 for remote)
*/

import { loadConfig, resolveUseJina } from "@plannotator/shared/config";
import { parseReviewArgs } from "@plannotator/shared/review-args";
import {
normalizeGoalSetupBundle,
type GoalSetupStage,
} from "@plannotator/shared/goal-setup";
import { statSync, existsSync, rmSync } from "fs";
import { tmpdir } from "os";
import {
getReviewApprovedPrompt,
getReviewDeniedSuffix,
getPlanDeniedPrompt,
getPlanToolName,
buildPlanFileRule,
} from "@plannotator/shared/prompts";

import { openBrowser } from "@plannotator/server/browser";
import { cleanupDaemonState, discoverDaemon, waitForDaemonShutdown } from "@plannotator/server/daemon/client";
import { startDaemonRuntime } from "@plannotator/server/daemon/runtime";
import { createDaemonSessionFactory } from "@plannotator/server/daemon/session-factory";
import { getDaemonStartCommand } from "@plannotator/server/daemon/start-command";
import { createDaemonBrowserAuthUrl } from "@plannotator/server/daemon/state";
import { formatRemoteShareNotice } from "@plannotator/server/share-url";
import { hostnameOrFallback } from "@plannotator/shared/project";
import { readImprovementHook } from "@plannotator/shared/improvement-hooks";
import { composeImproveContext } from "@plannotator/shared/pfm-reminder";
import { AGENT_CONFIG, type Origin } from "@plannotator/shared/agents";
import type { DaemonSessionSummary } from "@plannotator/shared/daemon-protocol";
import {
Expand Down Expand Up @@ -182,6 +171,7 @@ const APPROVED_PLAINTEXT_MARKER = "The user approved.";

function emitAnnotateOutcome(result: {
feedback: string;
prompt?: string;
exit?: boolean;
approved?: boolean;
}): void {
Expand All @@ -202,12 +192,13 @@ function emitAnnotateOutcome(result: {
}
return;
}
const output = result.prompt ?? result.feedback;
if (result.exit) return;
if (result.approved) {
console.log(APPROVED_PLAINTEXT_MARKER);
return;
}
if (result.feedback) console.log(result.feedback);
if (output) console.log(output);
}

if (isVersionInvocation(args)) {
Expand Down Expand Up @@ -452,10 +443,11 @@ async function cleanupDaemonStateForDaemonCommand(state: unknown): Promise<void>
}
}

async function cleanupDaemonStateForSessionCommand(state: unknown, options: { pluginError?: boolean }): Promise<void> {
async function cleanupDaemonStateForSessionCommand(state: unknown, options: { pluginError?: boolean; bestEffort?: boolean }): Promise<void> {
try {
await cleanupDaemonState(state);
} catch (err) {
if (options.bestEffort) throw err;
const fail = options.pluginError ? emitPluginError : emitCommandError;
fail("daemon-cleanup-failed", errorMessage(err));
}
Expand Down Expand Up @@ -534,8 +526,10 @@ function resolvePluginCwd(request: Partial<PluginBaseRequest>): string {
return cwd;
}

async function ensureDaemonClient(options: { pluginError?: boolean } = {}) {
const fail = options.pluginError ? emitPluginError : emitCommandError;
async function ensureDaemonClient(options: { pluginError?: boolean; bestEffort?: boolean } = {}) {
const fail = options.bestEffort
? (code: string, message: string) => { throw new Error(`${code}: ${message}`); }
: options.pluginError ? emitPluginError : emitCommandError;
const existing = await discoverDaemon();
if (existing.ok) return existing.client;
if (existing.state && (existing.code === "incompatible" || existing.code === "unhealthy")) {
Expand Down Expand Up @@ -709,13 +703,12 @@ async function runPluginPlanCommand(): Promise<void> {
async function runPluginAnnotateCommand(defaultMode: "annotate" | "annotate-last" = "annotate"): Promise<void> {
const request = await readPluginRequest<PluginAnnotateRequest>();
const origin = getPluginOrigin(request);
const useJina = resolveUseJina(request.noJina === true, loadConfig());
await runDaemonBackedPluginRequest({
...request,
action: defaultMode,
origin,
cwd: resolvePluginCwd(request),
useJina,
noJina: request.noJina,
jinaApiKey: process.env.JINA_API_KEY,
});
}
Expand Down Expand Up @@ -824,7 +817,6 @@ if (args[0] === "sessions") {
// CODE REVIEW MODE
// ============================================

const reviewArgs = parseReviewArgs(args.slice(1));
const outcome = await runDaemonSessionRequest({
action: "review",
origin: detectedOrigin,
Expand All @@ -833,17 +825,12 @@ if (args[0] === "sessions") {
sharingEnabled,
shareBaseUrl,
});
const result = outcome.result as { approved?: boolean; feedback?: string; exit?: boolean };
const result = outcome.result as { approved?: boolean; feedback?: string; prompt?: string; exit?: boolean };

if (result.exit) {
console.log("Review session closed without feedback.");
} else if (result.approved) {
console.log(getReviewApprovedPrompt(detectedOrigin));
} else {
console.log(result.feedback || "");
if (!reviewArgs.prUrl) {
console.log(getReviewDeniedSuffix(detectedOrigin));
}
console.log(result.prompt ?? result.feedback ?? "");
}
process.exit(0);

Expand All @@ -864,15 +851,14 @@ if (args[0] === "sessions") {
cwd: getInvocationCwd(),
args: rawFilePath,
noJina: cliNoJina,
useJina: resolveUseJina(cliNoJina, loadConfig()),
jinaApiKey: process.env.JINA_API_KEY,
gate: gateFlag,
renderHtml: renderHtmlFlag,
sharingEnabled,
shareBaseUrl,
pasteApiUrl,
});
emitAnnotateOutcome(outcome.result as { feedback: string; exit?: boolean; approved?: boolean });
emitAnnotateOutcome(outcome.result as { feedback: string; prompt?: string; exit?: boolean; approved?: boolean });
process.exit(0);

} else if (args[0] === "annotate-last" || args[0] === "last") {
Expand Down Expand Up @@ -973,7 +959,7 @@ if (args[0] === "sessions") {
pasteApiUrl,
});

emitAnnotateOutcome(outcome.result as { feedback: string; exit?: boolean; approved?: boolean });
emitAnnotateOutcome(outcome.result as { feedback: string; prompt?: string; exit?: boolean; approved?: boolean });
process.exit(0);

} else if (args[0] === "setup-goal") {
Expand Down Expand Up @@ -1066,22 +1052,17 @@ if (args[0] === "sessions") {
shareBaseUrl,
pasteApiUrl,
});
const result = outcome.result as { approved?: boolean; feedback?: string };
const result = outcome.result as { approved?: boolean; feedback?: string; prompt?: string };

// Output Copilot CLI permission decision format
if (result.approved) {
console.log(JSON.stringify({
permissionDecision: "allow",
}));
} else {
const feedback = getPlanDeniedPrompt("copilot-cli", undefined, {
toolName: getPlanToolName("copilot-cli"),
planFileRule: "",
feedback: result.feedback || "Plan changes requested",
});
console.log(JSON.stringify({
permissionDecision: "deny",
permissionDecisionReason: feedback,
permissionDecisionReason: result.prompt ?? result.feedback ?? "Plan changes requested",
}));
}

Expand Down Expand Up @@ -1132,7 +1113,7 @@ if (args[0] === "sessions") {
pasteApiUrl,
});

emitAnnotateOutcome(outcome.result as { feedback: string; exit?: boolean; approved?: boolean });
emitAnnotateOutcome(outcome.result as { feedback: string; prompt?: string; exit?: boolean; approved?: boolean });
process.exit(0);

} else if (args[0] === "improve-context") {
Expand All @@ -1141,21 +1122,22 @@ if (args[0] === "sessions") {
// ============================================
//
// Called by PreToolUse hook on EnterPlanMode.
// Composes any enabled context sources (compound improvement hook,
// Daemon composes any enabled context sources (compound improvement hook,
// PFM reminder) into a single additionalContext payload.
// Nothing enabled = exit 0 silently (passthrough).

await Bun.stdin.text();

const hook = readImprovementHook("enterplanmode-improve");
const pfmEnabled = loadConfig().pfmReminder === true;

const context = composeImproveContext({
pfmEnabled,
improvementHookContent: hook?.content ?? null,
});
let context: string | null = null;
try {
const client = await ensureDaemonClient({ bestEffort: true });
const data = await client.getJson("/daemon/improve-context") as { ok: boolean; context: string | null };
context = data.context;
} catch {
// Daemon unavailable — silently pass through
}

if (context === null) process.exit(0);
if (!context) process.exit(0);

console.log(JSON.stringify({
hookSpecificOutput: {
Expand Down Expand Up @@ -1214,19 +1196,15 @@ if (args[0] === "sessions") {
shareBaseUrl,
pasteApiUrl,
});
const result = outcome.result as { approved?: boolean; feedback?: string };
const result = outcome.result as { approved?: boolean; feedback?: string; prompt?: string };

if (result.approved) {
console.log("{}");
} else {
console.log(
JSON.stringify({
decision: "block",
reason: getPlanDeniedPrompt("codex", undefined, {
toolName: getPlanToolName("codex"),
planFileRule: "",
feedback: result.feedback || "Plan changes requested",
}),
reason: result.prompt ?? result.feedback ?? "Plan changes requested",
})
);
}
Expand Down Expand Up @@ -1266,6 +1244,7 @@ if (args[0] === "sessions") {
origin: isGemini ? "gemini-cli" : detectedOrigin,
cwd: getInvocationCwd(),
plan: planContent,
planFilePath: planFilename || undefined,
permissionMode,
sharingEnabled,
shareBaseUrl,
Expand All @@ -1274,6 +1253,7 @@ if (args[0] === "sessions") {
const result = outcome.result as {
approved?: boolean;
feedback?: string;
prompt?: string;
permissionMode?: string;
};

Expand All @@ -1285,11 +1265,7 @@ if (args[0] === "sessions") {
console.log(
JSON.stringify({
decision: "deny",
reason: getPlanDeniedPrompt("gemini-cli", undefined, {
toolName: getPlanToolName("gemini-cli"),
planFileRule: buildPlanFileRule(getPlanToolName("gemini-cli"), planFilename),
feedback: result.feedback || "Plan changes requested",
}),
reason: result.prompt ?? result.feedback ?? "Plan changes requested",
})
);
}
Expand Down Expand Up @@ -1323,11 +1299,7 @@ if (args[0] === "sessions") {
hookEventName: "PermissionRequest",
decision: {
behavior: "deny",
message: getPlanDeniedPrompt(detectedOrigin, undefined, {
toolName: getPlanToolName(detectedOrigin),
planFileRule: "",
feedback: result.feedback || "Plan changes requested",
}),
message: result.prompt ?? result.feedback ?? "Plan changes requested",
},
},
})
Expand Down
7 changes: 4 additions & 3 deletions apps/opencode-plugin/commands.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,14 @@ describe("handleAnnotateCommand", () => {
});
});

test("injects folder feedback using file metadata returned by the binary", async () => {
test("pipes server-composed prompt through to the agent session", async () => {
const serverPrompt = "# Markdown Annotations\n\nFolder: /repo/docs/Specs Folder\n\nPlease revise this section.\n\nPlease address the annotation feedback above.";
runPluginAnnotateMock.mockImplementationOnce(async (_binaryPath: string, _request: unknown) =>
createPluginSuccessResponse({
feedback: "Please revise this section.",
filePath: "/repo/docs/Specs Folder",
mode: "annotate-folder",
prompt: serverPrompt,
}),
);
const deps = makeDeps();
Expand All @@ -96,8 +98,7 @@ describe("handleAnnotateCommand", () => {

expect(deps.client.session.prompt).toHaveBeenCalledTimes(1);
const prompt = deps.client.session.prompt.mock.calls[0]?.[0] as any;
expect(prompt.body.parts[0].text).toContain("Folder: /repo/docs/Specs Folder");
expect(prompt.body.parts[0].text).toContain("Please revise this section.");
expect(prompt.body.parts[0].text).toBe(serverPrompt);
});
});

Expand Down
23 changes: 5 additions & 18 deletions apps/opencode-plugin/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,6 @@
* slash commands. Extracted from the event hook for modularity.
*/

import {
getReviewApprovedPrompt,
getReviewDeniedSuffix,
getAnnotateFileFeedbackPrompt,
} from "@plannotator/shared/prompts";
import { parseAnnotateArgs } from "@plannotator/shared/annotate-args";
import { parseReviewArgs } from "@plannotator/shared/review-args";
import type { PluginAgentInfo, PluginFeature } from "@plannotator/shared/plugin-protocol";
Expand Down Expand Up @@ -159,18 +154,14 @@ export async function handleReviewCommand(
return;
}

if (result.feedback) {
if (result.prompt || result.feedback) {
const sessionId = event.properties?.sessionID;

if (sessionId) {
const shouldSwitchAgent = result.agentSwitch && result.agentSwitch !== "disabled";
const targetAgent = result.agentSwitch || "build";

const message = result.approved
? getReviewApprovedPrompt("opencode")
: isPRMode
? result.feedback
: `${result.feedback}${getReviewDeniedSuffix("opencode")}`;
const message = result.prompt ?? result.feedback ?? "";

try {
await client.session.prompt({
Expand Down Expand Up @@ -227,7 +218,7 @@ export async function handleAnnotateCommand(
return;
}

if (result.feedback) {
if (result.prompt || result.feedback) {
const sessionId = event.properties?.sessionID;

if (sessionId) {
Expand All @@ -237,11 +228,7 @@ export async function handleAnnotateCommand(
body: {
parts: [{
type: "text",
text: getAnnotateFileFeedbackPrompt("opencode", undefined, {
fileHeader: result.mode === "annotate-folder" ? "Folder" : "File",
filePath: result.filePath ?? filePath,
feedback: result.feedback,
}),
text: result.prompt ?? result.feedback,
}],
},
});
Expand Down Expand Up @@ -339,6 +326,6 @@ export async function handleAnnotateLastCommand(
return null;
}

return result.feedback || null;
return result.prompt ?? (result.feedback || null);
}

3 changes: 1 addition & 2 deletions apps/opencode-plugin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ import {
getPlanApprovedPrompt,
getPlanApprovedWithNotesPrompt,
getPlanToolName,
getAnnotateMessageFeedbackPrompt,
} from "@plannotator/shared/prompts";
import { loadConfig } from "@plannotator/shared/config";
import { readImprovementHook } from "@plannotator/shared/improvement-hooks";
Expand Down Expand Up @@ -469,7 +468,7 @@ Do NOT proceed with implementation until your plan is approved.`);
body: {
parts: [{
type: "text",
text: getAnnotateMessageFeedbackPrompt("opencode", undefined, { feedback }),
text: feedback,
}],
},
});
Expand Down
8 changes: 0 additions & 8 deletions apps/pi-extension/assistant-message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,12 +63,4 @@ export function getLastAssistantMessageText(ctx: ExtensionContext): string | nul
return getLastAssistantMessageSnapshot(ctx)?.text ?? null;
}

export function hasSessionMovedPastEntry(ctx: ExtensionContext, entryId: string): boolean {
if (!ctx.isIdle()) return true;

const branch = getCurrentBranch(ctx);
const index = branch.findIndex((entry) => entry.id === entryId);
if (index === -1) return true;

return branch.slice(index + 1).some((entry) => entry.type === "message");
}
Loading