Files
Michael Ramos 98113182b5 feat(guide): reviewer-supplied extra instructions for Guided Review (#1267)
* feat(guide): reviewer-supplied extra instructions for Guided Review (#1265)

Adds a quiet, collapsed-by-default Custom instructions affordance to the
guide launch page. The text is APPENDED to the built-in organizer
methodology as a clearly delimited section (composeGuideMethodology) and
never replaces it; absent or blank instructions produce byte-identical
prompts to before. Persisted in a dedicated cookie
(plannotator-guide-instructions) so a standing team preference survives
sessions without bloating the plannotator.agents blob past the browser's
per-cookie limit.

Server side, the launch body gains an optional guide-only instructions
field (both the Bun and Pi node:http agent-jobs handlers accept and
thread it); prompt composition lives in the shared guide-review.ts that
vendor.sh already vendors to Pi, so both runtimes compose identically.
Text is capped at GUIDE_EXTRA_INSTRUCTIONS_MAX_CHARS (2000) server-side
and mirrored by the textarea maxLength. Repair launches deliberately
ignore instructions: a repair is a mechanical JSON fix, not a rewrite.

Tests pin the regression contract (empty input keeps prior prompt bytes),
appended-not-replacing composition, the length cap, repair isolation, and
the cookie round-trip via the storage backend seam.

* refactor(guide): store standing instructions server-side, not in a cookie

Review findings on the cookie approach (silent write failure past the
encoded 4KB per-cookie limit for multi-byte text) pointed at the real
design problem: the instructions are consumed by the SERVER at launch
time, so they belong in the data dir like review-skills.json, where no
size ceiling or encoding inflation exists and the preference follows
the machine instead of one browser profile.

New GET/PUT /api/agents/guide-instructions in both runtimes backed by
shared guide-instructions-store (vendored to Pi). Guide launches apply
the stored text when the body carries none; the launch page still sends
its live textarea value (explicit wins), so a just-typed preference can
never race the debounced save. The sidebar surface sends nothing and
inherits the stored text server-side. All cookie machinery removed.

Also folds in the review fixes: marker-tag-shaped strings in
instructions are defanged so first-match nonce recovery cannot be
hijacked by pasted examples.
2026-08-11 10:24:39 -07:00

65 lines
2.3 KiB
TypeScript

/**
* Guided Review standing instructions (#1265), persisted server-side in the
* data dir (like review-skills.json) rather than a browser cookie: the text
* is consumed by the SERVER at guide-launch time, a disk file has no
* per-cookie size ceiling or encoding inflation, and the preference follows
* the machine rather than one browser profile.
*/
import { mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { getPlannotatorDataDir } from "./data-dir";
import { GUIDE_EXTRA_INSTRUCTIONS_MAX_CHARS } from "./guide";
function instructionsPath(): string {
return join(getPlannotatorDataDir(), "guide-instructions.md");
}
function bound(value: string): string {
const trimmed = value.trim();
return trimmed.length > GUIDE_EXTRA_INSTRUCTIONS_MAX_CHARS
? trimmed.slice(0, GUIDE_EXTRA_INSTRUCTIONS_MAX_CHARS)
: trimmed;
}
/** Stored standing instructions, or "" when none are set or unreadable. */
export function readGuideInstructions(): string {
try {
return bound(readFileSync(instructionsPath(), "utf8"));
} catch {
return "";
}
}
/**
* Persist the instructions (trimmed, bounded) and return what was stored.
* Blank input deletes the file entirely rather than persisting whitespace.
*/
export function writeGuideInstructions(value: string): string {
const bounded = bound(value);
const path = instructionsPath();
if (bounded === "") {
try {
unlinkSync(path);
} catch {
// Already absent: deleting nothing is the desired end state.
}
return "";
}
mkdirSync(dirname(path), { recursive: true });
writeFileSync(path, bounded + "\n", "utf8");
return bounded;
}
/**
* The instructions a guide launch should use: explicit launch-body text wins
* (the launch page sends its live textarea value, so a just-typed preference
* can never race the persistence write), otherwise the stored standing
* instructions. Undefined when neither yields text, keeping instruction-less
* launches byte-identical to pre-feature prompts.
*/
export function resolveGuideLaunchInstructions(explicit?: unknown): string | undefined {
if (typeof explicit === "string" && explicit.trim() !== "") return explicit;
const stored = readGuideInstructions();
return stored === "" ? undefined : stored;
}