Skip to content

Commit 82636e1

Browse files
authored
Add interactive goal setup UI (#731)
* Add interactive goal setup UI * Refine goal interview skip and question flow * Fix review findings: recommendation combo, option-only recs, single deselect * Persist goal setup working JSON files * Refine goal setup copy and facts controls * Remove generated goal package from PR * Address goal setup review issues * Remove goal setup slash command adapters * Disable fact comment attachments * Fix goal setup fact comment state * Address goal setup review cleanup * Fix goal setup fact submission edge cases
1 parent f471ebf commit 82636e1

29 files changed

Lines changed: 3078 additions & 98 deletions

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,6 @@ opencode.json
5252
plannotator-local
5353
# Local research/reference docs (not for repo)
5454
/reference/
55+
# Local goal setup packages generated by the setup-goal skill.
56+
/goals/
5557
*.bun-build

apps/hook/dev-mock-api.ts

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -552,6 +552,9 @@ This change lands in section 3 of the contributor guide alongside the updated re
552552
const USE_DIFF_DEMO =
553553
process.env.VITE_DIFF_DEMO === "1" ||
554554
process.env.VITE_DIFF_DEMO === "true";
555+
const GOAL_SETUP_DEMO = process.env.VITE_GOAL_SETUP_DEMO;
556+
const USE_GOAL_SETUP_DEMO =
557+
GOAL_SETUP_DEMO === "interview" || GOAL_SETUP_DEMO === "facts";
555558

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

627630
if (req.url === '/api/plan') {
628631
res.setHeader('Content-Type', 'application/json');
632+
if (USE_GOAL_SETUP_DEMO) {
633+
res.end(JSON.stringify({
634+
plan: '',
635+
origin: 'claude-code',
636+
mode: 'goal-setup',
637+
sharingEnabled: false,
638+
goalSetup: GOAL_SETUP_DEMO === "facts" ? {
639+
stage: "facts",
640+
title: "Interactive goal setup facts",
641+
goalSlug: "interactive-goal-setup-ui",
642+
facts: [
643+
{
644+
id: "skill-batch",
645+
text: "The setup-goal skill should package all interview questions into one Plannotator UI session.",
646+
accepted: false,
647+
removed: false,
648+
recommendedAutomatedVerification: true,
649+
automatedVerification: true,
650+
},
651+
{
652+
id: "facts-verify",
653+
text: "Each fact can be accepted, edited, removed, commented on, and marked for automated verification.",
654+
accepted: false,
655+
removed: false,
656+
recommendedAutomatedVerification: true,
657+
automatedVerification: true,
658+
},
659+
{
660+
id: "header-submit",
661+
text: "Goal setup submission should use the Plannotator app header action area instead of local form buttons.",
662+
accepted: false,
663+
removed: false,
664+
recommendedAutomatedVerification: false,
665+
automatedVerification: false,
666+
},
667+
{
668+
id: "question-modes",
669+
text: "The interview UI should cover text answers, single-select choices, multi-select choices, and custom option entry.",
670+
accepted: false,
671+
removed: false,
672+
recommendedAutomatedVerification: true,
673+
automatedVerification: true,
674+
},
675+
{
676+
id: "previous",
677+
text: "Previously accepted facts remain visible in the facts review with their accepted state preserved.",
678+
accepted: true,
679+
removed: false,
680+
recommendedAutomatedVerification: false,
681+
automatedVerification: false,
682+
},
683+
{
684+
id: "bulk-accept",
685+
text: "The facts UI provides a single action to accept every visible fact while keeping the review open for final edits.",
686+
accepted: false,
687+
removed: false,
688+
recommendedAutomatedVerification: true,
689+
automatedVerification: true,
690+
},
691+
{
692+
id: "copy-export",
693+
text: "The interview and facts UIs can copy the current state as raw JSON or markdown for provenance and debugging.",
694+
accepted: false,
695+
removed: false,
696+
recommendedAutomatedVerification: false,
697+
automatedVerification: false,
698+
},
699+
],
700+
} : {
701+
stage: "interview",
702+
title: "Interactive goal setup interview",
703+
goalSlug: "interactive-goal-setup-ui",
704+
questions: [
705+
{
706+
id: "objective",
707+
prompt: "What is the primary outcome of this goal?",
708+
description: "One sentence that captures what 'done' looks like.",
709+
answerMode: "text",
710+
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.",
711+
},
712+
{
713+
id: "audience",
714+
prompt: "Which inferred audience assumption should change?",
715+
description: "The agent should not need basic confirmation here; only change this if the default is wrong.",
716+
answerMode: "single",
717+
recommendedAnswer: "Developers using Claude Code with Plannotator installed.",
718+
recommendedOptionIds: ["devs-cc"],
719+
options: [
720+
{ id: "devs-cc", label: "Developers on Claude Code" },
721+
{ id: "devs-oc", label: "Developers on OpenCode" },
722+
{ id: "devs-all", label: "All Plannotator users" },
723+
],
724+
},
725+
{
726+
id: "scope",
727+
prompt: "Which inferred scope items should stay or be added?",
728+
description: "Recommended items are based on the code paths the agent can infer. Add only missing nuance.",
729+
answerMode: "multi-custom",
730+
recommendedAnswer: "Skill text, interactive UI, server endpoints, and tests.",
731+
recommendedOptionIds: ["skill", "ui", "server", "tests"],
732+
options: [
733+
{ id: "skill", label: "Skill text" },
734+
{ id: "ui", label: "Interactive UI" },
735+
{ id: "server", label: "Server endpoints" },
736+
{ id: "tests", label: "Tests and fixtures" },
737+
],
738+
},
739+
{
740+
id: "launch",
741+
prompt: "What rollout constraint should override the default?",
742+
description: "Default is the smallest useful launch; choose a broader option only if runtime parity matters immediately.",
743+
answerMode: "single",
744+
recommendedOptionIds: ["claude-only"],
745+
options: [
746+
{ id: "claude-only", label: "Claude Code only" },
747+
{ id: "all-runtimes", label: "All runtimes (Claude Code, OpenCode, Pi)" },
748+
{ id: "prototype", label: "Prototype behind a dev flag" },
749+
],
750+
},
751+
{
752+
id: "risk",
753+
prompt: "Which risks should the plan explicitly address?",
754+
answerMode: "multi",
755+
recommendedOptionIds: ["runtime-parity", "data-loss"],
756+
options: [
757+
{ id: "runtime-parity", label: "Runtime parity", description: "Bun and Pi server endpoints stay mirrored." },
758+
{ id: "data-loss", label: "Answer data loss", description: "Edited answers survive until submission." },
759+
{ id: "header-actions", label: "Header action placement", description: "Submit/close matches existing patterns." },
760+
],
761+
},
762+
{
763+
id: "facts-ux",
764+
prompt: "How should fact review work?",
765+
answerMode: "text",
766+
recommendedAnswer: "Vertical list with per-fact accept, edit, remove, comment, and automated-verification toggle. Accepted facts hidden by default on re-review.",
767+
},
768+
{
769+
id: "out-of-scope",
770+
prompt: "Anything explicitly out of scope?",
771+
answerMode: "custom",
772+
required: false,
773+
},
774+
],
775+
},
776+
}));
777+
return;
778+
}
629779
res.end(JSON.stringify({
630780
plan: undefined, // Editor uses its own DIFF_DEMO_PLAN_CONTENT
631781
origin: 'claude-code',
@@ -636,6 +786,15 @@ export function devMockApi(): Plugin {
636786
return;
637787
}
638788

789+
if (req.url === '/api/goal-setup/submit' && req.method === 'POST') {
790+
req.on('data', () => {});
791+
req.on('end', () => {
792+
res.setHeader('Content-Type', 'application/json');
793+
res.end(JSON.stringify({ ok: true }));
794+
});
795+
return;
796+
}
797+
639798
if (req.url === '/api/plan/versions') {
640799
res.setHeader('Content-Type', 'application/json');
641800
res.end(JSON.stringify({

apps/hook/server/cli.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ describe("CLI top-level help", () => {
2323
expect(output).toContain("plannotator [--browser <name>]");
2424
expect(output).toContain("plannotator review [--git] [PR_URL]");
2525
expect(output).toContain("plannotator annotate <file.md | file.html | https://... | folder/>");
26+
expect(output).toContain("plannotator setup-goal <interview|facts>");
2627
expect(output).toContain("running 'plannotator' without arguments is for hook integration");
2728
});
2829
});
@@ -55,6 +56,7 @@ describe("interactive no-arg invocation", () => {
5556
expect(output).toContain("usually launched automatically by Claude Code hooks");
5657
expect(output).toContain("It expects hook JSON on stdin.");
5758
expect(output).toContain("plannotator review");
59+
expect(output).toContain("plannotator setup-goal interview bundle.json --json");
5860
expect(output).toContain("plannotator sessions");
5961
expect(output).toContain("Run 'plannotator --help' for top-level usage.");
6062
});

apps/hook/server/cli.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export function formatTopLevelHelp(): string {
2727
" plannotator [--browser <name>]",
2828
" plannotator review [--git] [PR_URL]",
2929
" plannotator annotate <file.md | file.html | https://... | folder/> [--no-jina] [--gate] [--json] [--hook]",
30+
" plannotator setup-goal <interview|facts> <bundle.json | -> [--json]",
3031
" plannotator last",
3132
" plannotator archive",
3233
" plannotator sessions",
@@ -45,6 +46,7 @@ export function formatInteractiveNoArgClarification(): string {
4546
"For interactive use, try:",
4647
" plannotator review",
4748
" plannotator annotate <file.md | file.html | https://...>",
49+
" plannotator setup-goal interview bundle.json --json",
4850
" plannotator last",
4951
" plannotator archive",
5052
" plannotator sessions",

apps/hook/server/index.ts

Lines changed: 87 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/**
22
* Plannotator CLI for Claude Code, Codex, Gemini CLI, and Copilot CLI
33
*
4-
* Supports eight modes:
4+
* Supports nine modes:
55
*
66
* 1. Plan Review (default, no args):
77
* - Spawned by Claude/Gemini/Codex hook entrypoints
@@ -37,7 +37,11 @@
3737
* - Annotate the last assistant message from a Copilot CLI session
3838
* - Parses events.jsonl from session state
3939
*
40-
* 8. Improve Context (`plannotator improve-context`):
40+
* 8. Goal Setup (`plannotator setup-goal interview|facts <bundle.json>`):
41+
* - Opens the bundled question or facts acceptance UI
42+
* - Outputs structured JSON for setup-goal workflows
43+
*
44+
* 9. Improve Context (`plannotator improve-context`):
4145
* - Spawned by PreToolUse hook on EnterPlanMode
4246
* - Reads improvement hook file from ~/.plannotator/hooks/
4347
* - Returns additionalContext or silently passes through
@@ -64,9 +68,17 @@ import {
6468
startAnnotateServer,
6569
handleAnnotateServerReady,
6670
} from "@plannotator/server/annotate";
71+
import {
72+
startGoalSetupServer,
73+
handleGoalSetupServerReady,
74+
} from "@plannotator/server/goal-setup";
6775
import { type DiffType, prepareLocalReviewDiff, gitRuntime } from "@plannotator/server/vcs";
6876
import { loadConfig, resolveDefaultDiffType, resolveUseJina } from "@plannotator/shared/config";
6977
import { parseReviewArgs } from "@plannotator/shared/review-args";
78+
import {
79+
normalizeGoalSetupBundle,
80+
type GoalSetupStage,
81+
} from "@plannotator/shared/goal-setup";
7082
import { stripAtPrefix, resolveAtReference } from "@plannotator/shared/at-reference";
7183
import { htmlToMarkdown } from "@plannotator/shared/html-to-markdown";
7284
import { urlToMarkdown, isConvertedSource } from "@plannotator/shared/url-to-markdown";
@@ -205,6 +217,17 @@ function emitAnnotateOutcome(result: {
205217
if (result.feedback) console.log(result.feedback);
206218
}
207219

220+
async function loadGoalSetupBundle(
221+
stage: GoalSetupStage,
222+
bundlePath: string
223+
) {
224+
const raw =
225+
bundlePath === "-"
226+
? await Bun.stdin.text()
227+
: await Bun.file(path.resolve(bundlePath)).text();
228+
return normalizeGoalSetupBundle(JSON.parse(raw), stage);
229+
}
230+
208231
if (isVersionInvocation(args)) {
209232
console.log(formatVersion());
210233
process.exit(0);
@@ -297,6 +320,68 @@ if (args[0] === "sessions") {
297320
console.error(`\nReopen with: plannotator sessions --open [N]`);
298321
process.exit(0);
299322

323+
} else if (args[0] === "setup-goal") {
324+
// ============================================
325+
// GOAL SETUP MODE
326+
// ============================================
327+
328+
const stage = args[1] as GoalSetupStage | undefined;
329+
const bundlePath = args[2];
330+
331+
if ((stage !== "interview" && stage !== "facts") || !bundlePath) {
332+
console.error(
333+
"Usage: plannotator setup-goal <interview|facts> <bundle.json | -> [--json]"
334+
);
335+
process.exit(1);
336+
}
337+
338+
let bundle: Awaited<ReturnType<typeof loadGoalSetupBundle>>;
339+
try {
340+
bundle = await loadGoalSetupBundle(stage, bundlePath);
341+
} catch (err) {
342+
console.error(
343+
`Failed to load goal setup bundle: ${err instanceof Error ? err.message : String(err)}`
344+
);
345+
process.exit(1);
346+
}
347+
348+
const goalProject = (await detectProjectName()) ?? "_unknown";
349+
350+
const server = await startGoalSetupServer({
351+
bundle,
352+
origin: detectedOrigin,
353+
htmlContent: planHtmlContent,
354+
onReady: (url, isRemote, port) => {
355+
handleGoalSetupServerReady(url, isRemote, port);
356+
},
357+
});
358+
359+
registerSession({
360+
pid: process.pid,
361+
port: server.port,
362+
url: server.url,
363+
mode: "goal-setup",
364+
project: goalProject,
365+
startedAt: new Date().toISOString(),
366+
label: `goal-setup-${bundle.stage}-${bundle.goalSlug || goalProject}`,
367+
});
368+
369+
const result = await server.waitForDecision();
370+
await Bun.sleep(800);
371+
server.stop();
372+
373+
if (result.exit) {
374+
console.log(JSON.stringify({ decision: "dismissed", stage: bundle.stage }));
375+
} else if (result.result) {
376+
const output = {
377+
decision: "submitted",
378+
stage: result.result.stage,
379+
result: result.result,
380+
};
381+
console.log(jsonFlag ? JSON.stringify(output) : JSON.stringify(output, null, 2));
382+
}
383+
process.exit(0);
384+
300385
} else if (args[0] === "review") {
301386
// ============================================
302387
// CODE REVIEW MODE

apps/marketing/src/lib/shortcutReference.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import { planReviewSurface, annotateSurface } from '../../../../packages/editor/shortcuts';
1+
import { planReviewSurface, annotateSurface, goalSetupSurface } from '../../../../packages/editor/shortcuts';
22
import { codeReviewSurface } from '../../../../packages/review-editor/shortcuts';
33
import { listRegistryShortcutSections } from '../../../../packages/ui/shortcuts';
44
import type { ShortcutSurface } from '../../../../packages/ui/shortcuts';
55

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

8-
const allSurfaces: ShortcutSurface[] = [planReviewSurface, annotateSurface, codeReviewSurface];
8+
const allSurfaces: ShortcutSurface[] = [planReviewSurface, annotateSurface, goalSetupSurface, codeReviewSurface];
99

1010
export const shortcutReferenceSurfaces = allSurfaces.map((surface) => ({
1111
...surface,

apps/pi-extension/vendor.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
set -euo pipefail
55
cd "$(dirname "$0")"
66

7+
rm -rf generated
78
mkdir -p generated generated/ai/providers
89

910
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

0 commit comments

Comments
 (0)