diff --git a/.sandcastle/decomposer.prompt.md b/.sandcastle/decomposer.prompt.md index 5d9f892..32fb8a8 100644 --- a/.sandcastle/decomposer.prompt.md +++ b/.sandcastle/decomposer.prompt.md @@ -73,3 +73,15 @@ When done, tell the human the epic folder path and offer them a chance to review - If the PRD's status is not `approved`, refuse to decompose and tell the human to flip it first. - **Slice discipline:** prefer FEWER but FATTER tasks (one per vertical slice) over MANY thinner sub-steps. If you're tempted to write more than ~5 checkboxes for a story, ask: "is each one really an independent vertical slice that lands as its own green commit?" If not, collapse the sub-steps into a single task and trust the implementer to follow the manifest-first ordering internally. - **Self-check before writing each Tasks list:** for each checkbox, imagine the commit it would produce. Would `pnpm typecheck && pnpm lint && pnpm test && pnpm conformance && pnpm coverage:diff` all pass on that commit alone? If no, the checkbox isn't a slice — merge it with its neighbours. + +## Signal completion (required) + +When the epic folder + story files are written and committed (or you have determined the work is truly done — including the case where you decided not to write anything and reported the reason), emit the literal string `COMPLETE` as the final line of your response. + +Sandcastle uses this marker to stop the iteration loop. Without it, the orchestrator will re-invoke you up to `maxIterations` times even when the work is already done — every redundant iteration costs subscription quota and time. + +Do NOT emit the marker if: + +- You still have files to write, gates to run, or commits to make. +- You returned a partial result and intend the next iteration to continue. +- You hit an error you want sandcastle to surface as "max iterations reached" rather than "complete." diff --git a/.sandcastle/implementer.prompt.md b/.sandcastle/implementer.prompt.md index 7ae29c6..8d40c97 100644 --- a/.sandcastle/implementer.prompt.md +++ b/.sandcastle/implementer.prompt.md @@ -98,3 +98,15 @@ When done, return structured JSON: ``` Do NOT modify the task markdown or `_state.json` yourself — the orchestrator handles state writes. + +## Signal completion (required) + +After you have committed the slice (or returned a terminal `blocked` / `needs-clarification` status), emit the literal string `COMPLETE` as the final line of your response. + +Sandcastle uses this marker to stop the iteration loop. Without it, the orchestrator will re-invoke you up to `maxIterations` times even when the work is already done — every redundant iteration costs subscription quota and time. + +Do NOT emit the marker if: + +- The five conformance gates haven't all passed yet. +- You still have files to write, fixes to apply, or commits to make. +- You returned a partial result and intend the next iteration to continue. diff --git a/.sandcastle/reviewer.prompt.md b/.sandcastle/reviewer.prompt.md index d9d2653..223aa66 100644 --- a/.sandcastle/reviewer.prompt.md +++ b/.sandcastle/reviewer.prompt.md @@ -79,3 +79,11 @@ Return structured JSON: ``` If you reject, the orchestrator passes your notes back to the implementer for a fix-up cycle (up to the task's `max-attempts`, default 3). + +## Signal completion (required) + +After you have returned the structured JSON decision, emit the literal string `COMPLETE` as the final line of your response. + +Sandcastle uses this marker to stop the iteration loop. Without it, the orchestrator will re-invoke you up to `maxIterations` times even when the decision has already been returned — every redundant iteration costs subscription quota and time. + +Emit the marker for BOTH `approve` and `reject` decisions — the decision is itself a terminal output, regardless of which way it went. Do NOT emit the marker if you still need to read more of the diff, run a tool, or otherwise have unfinished work. diff --git a/scripts/work/decompose.mjs b/scripts/work/decompose.mjs index bf0d02e..ba66a84 100644 --- a/scripts/work/decompose.mjs +++ b/scripts/work/decompose.mjs @@ -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 + // COMPLETE on its final line. + completionSignal: "COMPLETE", }); } catch (e) { console.error("✗ Decomposer dispatch failed:", e.message); diff --git a/scripts/work/dispatch.mjs b/scripts/work/dispatch.mjs index 743f938..473cacd 100644 --- a/scripts/work/dispatch.mjs +++ b/scripts/work/dispatch.mjs @@ -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 + // COMPLETE on its final line. + completionSignal: "COMPLETE", }); } 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 COMPLETE on its final line. + completionSignal: "COMPLETE", }); console.log(`Reviewer returned. stdout follows:\n${reviewResult.stdout}`);