Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,6 @@ opencode.json
plannotator-local
# Local research/reference docs (not for repo)
/reference/
# Local goal setup packages generated by the setup-goal skill.
/goals/
*.bun-build
159 changes: 159 additions & 0 deletions apps/hook/dev-mock-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -552,6 +552,9 @@ This change lands in section 3 of the contributor guide alongside the updated re
const USE_DIFF_DEMO =
process.env.VITE_DIFF_DEMO === "1" ||
process.env.VITE_DIFF_DEMO === "true";
const GOAL_SETUP_DEMO = process.env.VITE_GOAL_SETUP_DEMO;
const USE_GOAL_SETUP_DEMO =
GOAL_SETUP_DEMO === "interview" || GOAL_SETUP_DEMO === "facts";

const PLAN_V1 = USE_DIFF_DEMO ? PLAN_V1_DIFF_TEST : PLAN_V1_DEFAULT;
const PLAN_V2 = USE_DIFF_DEMO ? PLAN_V2_DIFF_TEST : PLAN_V2_DEFAULT;
Expand Down Expand Up @@ -626,6 +629,153 @@ export function devMockApi(): Plugin {

if (req.url === '/api/plan') {
res.setHeader('Content-Type', 'application/json');
if (USE_GOAL_SETUP_DEMO) {
res.end(JSON.stringify({
plan: '',
origin: 'claude-code',
mode: 'goal-setup',
sharingEnabled: false,
goalSetup: GOAL_SETUP_DEMO === "facts" ? {
stage: "facts",
title: "Interactive goal setup facts",
goalSlug: "interactive-goal-setup-ui",
facts: [
{
id: "skill-batch",
text: "The setup-goal skill should package all interview questions into one Plannotator UI session.",
accepted: false,
removed: false,
recommendedAutomatedVerification: true,
automatedVerification: true,
},
{
id: "facts-verify",
text: "Each fact can be accepted, edited, removed, commented on, and marked for automated verification.",
accepted: false,
removed: false,
recommendedAutomatedVerification: true,
automatedVerification: true,
},
{
id: "header-submit",
text: "Goal setup submission should use the Plannotator app header action area instead of local form buttons.",
accepted: false,
removed: false,
recommendedAutomatedVerification: false,
automatedVerification: false,
},
{
id: "question-modes",
text: "The interview UI should cover text answers, single-select choices, multi-select choices, and custom option entry.",
accepted: false,
removed: false,
recommendedAutomatedVerification: true,
automatedVerification: true,
},
{
id: "previous",
text: "Previously accepted facts remain visible in the facts review with their accepted state preserved.",
accepted: true,
removed: false,
recommendedAutomatedVerification: false,
automatedVerification: false,
},
{
id: "bulk-accept",
text: "The facts UI provides a single action to accept every visible fact while keeping the review open for final edits.",
accepted: false,
removed: false,
recommendedAutomatedVerification: true,
automatedVerification: true,
},
{
id: "copy-export",
text: "The interview and facts UIs can copy the current state as raw JSON or markdown for provenance and debugging.",
accepted: false,
removed: false,
recommendedAutomatedVerification: false,
automatedVerification: false,
},
],
} : {
stage: "interview",
title: "Interactive goal setup interview",
goalSlug: "interactive-goal-setup-ui",
questions: [
{
id: "objective",
prompt: "What is the primary outcome of this goal?",
description: "One sentence that captures what 'done' looks like.",
answerMode: "text",
recommendedAnswer: "A bundled goal setup UI where agents launch one browser session for interview Q&A and a second for facts acceptance, replacing multi-turn chat prompting.",
},
{
id: "audience",
prompt: "Which inferred audience assumption should change?",
description: "The agent should not need basic confirmation here; only change this if the default is wrong.",
answerMode: "single",
recommendedAnswer: "Developers using Claude Code with Plannotator installed.",
recommendedOptionIds: ["devs-cc"],
options: [
{ id: "devs-cc", label: "Developers on Claude Code" },
{ id: "devs-oc", label: "Developers on OpenCode" },
{ id: "devs-all", label: "All Plannotator users" },
],
},
{
id: "scope",
prompt: "Which inferred scope items should stay or be added?",
description: "Recommended items are based on the code paths the agent can infer. Add only missing nuance.",
answerMode: "multi-custom",
recommendedAnswer: "Skill text, interactive UI, server endpoints, and tests.",
recommendedOptionIds: ["skill", "ui", "server", "tests"],
options: [
{ id: "skill", label: "Skill text" },
{ id: "ui", label: "Interactive UI" },
{ id: "server", label: "Server endpoints" },
{ id: "tests", label: "Tests and fixtures" },
],
},
{
id: "launch",
prompt: "What rollout constraint should override the default?",
description: "Default is the smallest useful launch; choose a broader option only if runtime parity matters immediately.",
answerMode: "single",
recommendedOptionIds: ["claude-only"],
options: [
{ id: "claude-only", label: "Claude Code only" },
{ id: "all-runtimes", label: "All runtimes (Claude Code, OpenCode, Pi)" },
{ id: "prototype", label: "Prototype behind a dev flag" },
],
},
{
id: "risk",
prompt: "Which risks should the plan explicitly address?",
answerMode: "multi",
recommendedOptionIds: ["runtime-parity", "data-loss"],
options: [
{ id: "runtime-parity", label: "Runtime parity", description: "Bun and Pi server endpoints stay mirrored." },
{ id: "data-loss", label: "Answer data loss", description: "Edited answers survive until submission." },
{ id: "header-actions", label: "Header action placement", description: "Submit/close matches existing patterns." },
],
},
{
id: "facts-ux",
prompt: "How should fact review work?",
answerMode: "text",
recommendedAnswer: "Vertical list with per-fact accept, edit, remove, comment, and automated-verification toggle. Accepted facts hidden by default on re-review.",
},
{
id: "out-of-scope",
prompt: "Anything explicitly out of scope?",
answerMode: "custom",
required: false,
},
],
},
}));
return;
}
res.end(JSON.stringify({
plan: undefined, // Editor uses its own DIFF_DEMO_PLAN_CONTENT
origin: 'claude-code',
Expand All @@ -636,6 +786,15 @@ export function devMockApi(): Plugin {
return;
}

if (req.url === '/api/goal-setup/submit' && req.method === 'POST') {
req.on('data', () => {});
req.on('end', () => {
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ ok: true }));
});
return;
}

if (req.url === '/api/plan/versions') {
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({
Expand Down
2 changes: 2 additions & 0 deletions apps/hook/server/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ describe("CLI top-level help", () => {
expect(output).toContain("plannotator [--browser <name>]");
expect(output).toContain("plannotator review [--git] [PR_URL]");
expect(output).toContain("plannotator annotate <file.md | file.html | https://... | folder/>");
expect(output).toContain("plannotator setup-goal <interview|facts>");
expect(output).toContain("running 'plannotator' without arguments is for hook integration");
});
});
Expand Down Expand Up @@ -55,6 +56,7 @@ describe("interactive no-arg invocation", () => {
expect(output).toContain("usually launched automatically by Claude Code hooks");
expect(output).toContain("It expects hook JSON on stdin.");
expect(output).toContain("plannotator review");
expect(output).toContain("plannotator setup-goal interview bundle.json --json");
expect(output).toContain("plannotator sessions");
expect(output).toContain("Run 'plannotator --help' for top-level usage.");
});
Expand Down
2 changes: 2 additions & 0 deletions apps/hook/server/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export function formatTopLevelHelp(): string {
" plannotator [--browser <name>]",
" plannotator review [--git] [PR_URL]",
" plannotator annotate <file.md | file.html | https://... | folder/> [--no-jina] [--gate] [--json] [--hook]",
" plannotator setup-goal <interview|facts> <bundle.json | -> [--json]",
" plannotator last",
" plannotator archive",
" plannotator sessions",
Expand All @@ -45,6 +46,7 @@ export function formatInteractiveNoArgClarification(): string {
"For interactive use, try:",
" plannotator review",
" plannotator annotate <file.md | file.html | https://...>",
" plannotator setup-goal interview bundle.json --json",
" plannotator last",
" plannotator archive",
" plannotator sessions",
Expand Down
89 changes: 87 additions & 2 deletions apps/hook/server/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* Plannotator CLI for Claude Code, Codex, Gemini CLI, and Copilot CLI
*
* Supports eight modes:
* Supports nine modes:
*
* 1. Plan Review (default, no args):
* - Spawned by Claude/Gemini/Codex hook entrypoints
Expand Down Expand Up @@ -37,7 +37,11 @@
* - Annotate the last assistant message from a Copilot CLI session
* - Parses events.jsonl from session state
*
* 8. Improve Context (`plannotator improve-context`):
* 8. Goal Setup (`plannotator setup-goal interview|facts <bundle.json>`):
* - Opens the bundled question or facts acceptance UI
* - Outputs structured JSON for setup-goal workflows
*
* 9. Improve Context (`plannotator improve-context`):
* - Spawned by PreToolUse hook on EnterPlanMode
* - Reads improvement hook file from ~/.plannotator/hooks/
* - Returns additionalContext or silently passes through
Expand All @@ -64,9 +68,17 @@ import {
startAnnotateServer,
handleAnnotateServerReady,
} from "@plannotator/server/annotate";
import {
startGoalSetupServer,
handleGoalSetupServerReady,
} from "@plannotator/server/goal-setup";
import { type DiffType, prepareLocalReviewDiff, gitRuntime } from "@plannotator/server/vcs";
import { loadConfig, resolveDefaultDiffType, resolveUseJina } from "@plannotator/shared/config";
import { parseReviewArgs } from "@plannotator/shared/review-args";
import {
normalizeGoalSetupBundle,
type GoalSetupStage,
} from "@plannotator/shared/goal-setup";
import { stripAtPrefix, resolveAtReference } from "@plannotator/shared/at-reference";
import { htmlToMarkdown } from "@plannotator/shared/html-to-markdown";
import { urlToMarkdown, isConvertedSource } from "@plannotator/shared/url-to-markdown";
Expand Down Expand Up @@ -205,6 +217,17 @@ function emitAnnotateOutcome(result: {
if (result.feedback) console.log(result.feedback);
}

async function loadGoalSetupBundle(
stage: GoalSetupStage,
bundlePath: string
) {
const raw =
bundlePath === "-"
? await Bun.stdin.text()
: await Bun.file(path.resolve(bundlePath)).text();
return normalizeGoalSetupBundle(JSON.parse(raw), stage);
}

if (isVersionInvocation(args)) {
console.log(formatVersion());
process.exit(0);
Expand Down Expand Up @@ -297,6 +320,68 @@ if (args[0] === "sessions") {
console.error(`\nReopen with: plannotator sessions --open [N]`);
process.exit(0);

} else if (args[0] === "setup-goal") {
// ============================================
// GOAL SETUP MODE
// ============================================

const stage = args[1] as GoalSetupStage | undefined;
const bundlePath = args[2];

if ((stage !== "interview" && stage !== "facts") || !bundlePath) {
console.error(
"Usage: plannotator setup-goal <interview|facts> <bundle.json | -> [--json]"
);
process.exit(1);
}

let bundle: Awaited<ReturnType<typeof loadGoalSetupBundle>>;
try {
bundle = await loadGoalSetupBundle(stage, bundlePath);
} catch (err) {
console.error(
`Failed to load goal setup bundle: ${err instanceof Error ? err.message : String(err)}`
);
process.exit(1);
}

const goalProject = (await detectProjectName()) ?? "_unknown";

const server = await startGoalSetupServer({
bundle,
origin: detectedOrigin,
htmlContent: planHtmlContent,
onReady: (url, isRemote, port) => {
handleGoalSetupServerReady(url, isRemote, port);
},
});

registerSession({
pid: process.pid,
port: server.port,
url: server.url,
mode: "goal-setup",
project: goalProject,
startedAt: new Date().toISOString(),
label: `goal-setup-${bundle.stage}-${bundle.goalSlug || goalProject}`,
});

const result = await server.waitForDecision();
await Bun.sleep(800);
server.stop();

if (result.exit) {
console.log(JSON.stringify({ decision: "dismissed", stage: bundle.stage }));
} else if (result.result) {
const output = {
decision: "submitted",
stage: result.result.stage,
result: result.result,
};
console.log(jsonFlag ? JSON.stringify(output) : JSON.stringify(output, null, 2));
}
process.exit(0);

} else if (args[0] === "review") {
// ============================================
// CODE REVIEW MODE
Expand Down
4 changes: 2 additions & 2 deletions apps/marketing/src/lib/shortcutReference.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { planReviewSurface, annotateSurface } from '../../../../packages/editor/shortcuts';
import { planReviewSurface, annotateSurface, goalSetupSurface } from '../../../../packages/editor/shortcuts';
import { codeReviewSurface } from '../../../../packages/review-editor/shortcuts';
import { listRegistryShortcutSections } from '../../../../packages/ui/shortcuts';
import type { ShortcutSurface } from '../../../../packages/ui/shortcuts';

const slugify = (value: string) => value.toLowerCase().replace(/\s+/g, '-');

const allSurfaces: ShortcutSurface[] = [planReviewSurface, annotateSurface, codeReviewSurface];
const allSurfaces: ShortcutSurface[] = [planReviewSurface, annotateSurface, goalSetupSurface, codeReviewSurface];

export const shortcutReferenceSurfaces = allSurfaces.map((surface) => ({
...surface,
Expand Down
1 change: 1 addition & 0 deletions apps/pi-extension/vendor.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
set -euo pipefail
cd "$(dirname "$0")"

rm -rf generated
mkdir -p generated generated/ai/providers

for f in feedback-templates prompts review-core jj-core vcs-core review-args storage draft project pr-types pr-provider pr-stack pr-github pr-gitlab checklist integrations-common repo reference-common favicon code-file resolve-file config external-annotation agent-jobs worktree worktree-pool html-to-markdown url-to-markdown tour annotate-args at-reference pfm-reminder improvement-hooks code-nav; do
Expand Down
Loading
Loading