mirror of
https://github.com/backnotprop/plannotator.git
synced 2026-09-14 14:17:26 +08:00
98113182b5
* 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.
65 lines
2.3 KiB
TypeScript
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;
|
|
}
|