mirror of
https://github.com/backnotprop/plannotator.git
synced 2026-09-14 14:17:26 +08:00
26681abb8c
The Approve with Notes flow (#1092) emits decision:"approved" with a feedback field, but both adapters collapsed every approved decision to a bare "Approved." -- the reviewer's notes were silently lost. Amp had two drop sites: formatAnnotationFeedback returned null for anything not "annotated", and handleAnnotateResult early-returned on approved before formatting was ever reached. Droid's emitAnnotateDecision printed only "Approved." Both adapters now emit the shared approved-with-notes prompt (mirroring DEFAULT_ANNOTATE_APPROVED_WITH_NOTES_PROMPT in packages/shared/prompts.ts); note-less approvals keep the old behavior. The amp config override key approvedWithNotes matches the shared prompt config shape. Fixes #1137
137 lines
4.0 KiB
JavaScript
Executable File
137 lines
4.0 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
|
|
const childProcess = require("node:child_process");
|
|
const fs = require("node:fs");
|
|
const path = require("node:path");
|
|
|
|
const bundledRepoBin = path.resolve(__dirname, "..", "..", "..", "bin", "plannotator.js");
|
|
|
|
function findRepoBin(startDir) {
|
|
let dir = path.resolve(startDir);
|
|
|
|
while (true) {
|
|
const packageJsonPath = path.join(dir, "package.json");
|
|
const candidateBin = path.join(dir, "bin", "plannotator.js");
|
|
|
|
if (fs.existsSync(packageJsonPath) && fs.existsSync(candidateBin)) {
|
|
try {
|
|
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
|
|
if (pkg && pkg.name === "plannotator") {
|
|
return candidateBin;
|
|
}
|
|
} catch {
|
|
// Ignore malformed package.json while walking up.
|
|
}
|
|
}
|
|
|
|
const parent = path.dirname(dir);
|
|
if (parent === dir) break;
|
|
dir = parent;
|
|
}
|
|
|
|
return fs.existsSync(bundledRepoBin) ? bundledRepoBin : null;
|
|
}
|
|
|
|
function writeIfPresent(stream, text) {
|
|
if (!text) return;
|
|
stream.write(text.endsWith("\n") ? text : `${text}\n`);
|
|
}
|
|
|
|
function runPlannotator(args) {
|
|
const repoBin = findRepoBin(process.cwd());
|
|
const env = {
|
|
...process.env,
|
|
PLANNOTATOR_CWD: process.cwd(),
|
|
PLANNOTATOR_ORIGIN: "droid",
|
|
};
|
|
|
|
let result = childProcess.spawnSync("plannotator", args, {
|
|
encoding: "utf8",
|
|
env,
|
|
});
|
|
|
|
if (result.error && result.error.code === "ENOENT" && repoBin) {
|
|
result = childProcess.spawnSync(process.execPath, [repoBin, ...args], {
|
|
encoding: "utf8",
|
|
env,
|
|
});
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
function exitWithFailure(result, invocation) {
|
|
writeIfPresent(process.stderr, result.stderr);
|
|
writeIfPresent(process.stderr, result.stdout);
|
|
|
|
if (result.error && result.error.code === "ENOENT") {
|
|
writeIfPresent(
|
|
process.stderr,
|
|
[
|
|
`Could not run \`${invocation}\` because the \`plannotator\` CLI is not installed or not on PATH.`,
|
|
"Install it first: https://plannotator.ai/docs/getting-started/installation/",
|
|
].join("\n"),
|
|
);
|
|
} else if (result.error) {
|
|
writeIfPresent(process.stderr, `${invocation} failed: ${result.error.message}`);
|
|
}
|
|
|
|
process.exit(typeof result.status === "number" ? result.status : 1);
|
|
}
|
|
|
|
function emitAnnotateDecision(rawOutput, heading) {
|
|
const output = rawOutput.trim();
|
|
if (!output) {
|
|
process.stdout.write("Annotation session closed.\n");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const parsed = JSON.parse(output);
|
|
if (parsed && typeof parsed === "object") {
|
|
if (parsed.decision === "approved") {
|
|
// Approve-with-Notes (#1092): `approved` may carry reviewer notes in
|
|
// `feedback`. Only a note-less approval is a plain "Approved."
|
|
const feedback = typeof parsed.feedback === "string" ? parsed.feedback.trim() : "";
|
|
if (!feedback) {
|
|
process.stdout.write("Approved.\n");
|
|
return;
|
|
}
|
|
// Mirrors DEFAULT_ANNOTATE_APPROVED_WITH_NOTES_PROMPT in
|
|
// packages/shared/prompts.ts. Keep the two in sync.
|
|
process.stdout.write(
|
|
`# Approved with Notes\n\nThe artifact is approved. The notes below are non-blocking guidance, not a request for another revision.\n\n${feedback}\n\nDo not revise or reopen the artifact solely because of these notes unless the user explicitly requests it. Carry the notes into subsequent work where applicable.\n`,
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (parsed.decision === "dismissed") {
|
|
process.stdout.write("Annotation session closed.\n");
|
|
return;
|
|
}
|
|
|
|
if (parsed.decision === "annotated") {
|
|
const feedback = typeof parsed.feedback === "string" ? parsed.feedback.trim() : "";
|
|
if (!feedback) {
|
|
process.stdout.write("Annotation session closed.\n");
|
|
return;
|
|
}
|
|
process.stdout.write(
|
|
`# ${heading}\n\n${feedback}\n\nPlease address the annotation feedback above.\n`,
|
|
);
|
|
return;
|
|
}
|
|
}
|
|
} catch {
|
|
// Fall back to the raw output below.
|
|
}
|
|
|
|
writeIfPresent(process.stdout, output);
|
|
}
|
|
|
|
module.exports = {
|
|
emitAnnotateDecision,
|
|
exitWithFailure,
|
|
runPlannotator,
|
|
};
|