fix(work): emit completion signal to stop sandcastle agent loops

Sandcastle re-invokes agents up to maxIterations even when the work is
already done — the decomposer was looping 4x re-writing the same epic
on every dispatch. Two halves to the fix:

- Pass completionSignal: "<promise>COMPLETE</promise>" explicitly on
  all three run() calls (decompose, implementer, reviewer). Makes the
  contract visible alongside maxIterations instead of relying on
  sandcastle's default.
- Append a "Signal completion (required)" section to each prompt
  telling the agent to emit the literal marker as its final line when
  the work is genuinely done, plus a "do NOT emit if..." list to
  discourage premature signaling.
This commit is contained in:
2026-05-13 19:11:44 +02:00
parent d6bf2f638f
commit eadbb7ebd9
5 changed files with 47 additions and 0 deletions

View File

@@ -166,6 +166,11 @@ export async function executeDecompose(prdId, prdPath, prdText) {
// context, write epic + stories, commit); 10 iterations is enough
// room. Tune via env SANDCASTLE_DECOMPOSE_ITERATIONS.
maxIterations: Number(process.env.SANDCASTLE_DECOMPOSE_ITERATIONS ?? 10),
// Stop iterating the moment the agent emits this marker. Without it,
// sandcastle re-invokes the model up to maxIterations even when the
// work is already done — the prompt instructs the agent to emit
// <promise>COMPLETE</promise> on its final line.
completionSignal: "<promise>COMPLETE</promise>",
});
} catch (e) {
console.error("✗ Decomposer dispatch failed:", e.message);

View File

@@ -237,6 +237,11 @@ async function executeDispatch() {
maxIterations: Number(
process.env.SANDCASTLE_IMPLEMENTER_ITERATIONS ?? 30,
),
// Stop iterating the moment the agent emits this marker. Without it,
// sandcastle re-invokes the model up to maxIterations even when the
// work is already done — the prompt instructs the agent to emit
// <promise>COMPLETE</promise> on its final line.
completionSignal: "<promise>COMPLETE</promise>",
});
} catch (e) {
console.error("✗ Implementer dispatch failed:", e.message);
@@ -290,6 +295,11 @@ async function executeDispatch() {
// Smaller surface than the implementer; 10 iterations is plenty.
// Tune via env SANDCASTLE_REVIEWER_ITERATIONS.
maxIterations: Number(process.env.SANDCASTLE_REVIEWER_ITERATIONS ?? 10),
// Stop iterating the moment the agent emits this marker. Without it,
// sandcastle re-invokes the model up to maxIterations even when the
// decision has already been returned — the prompt instructs the agent
// to emit <promise>COMPLETE</promise> on its final line.
completionSignal: "<promise>COMPLETE</promise>",
});
console.log(`Reviewer returned. stdout follows:\n${reviewResult.stdout}`);