@tritard/waterbrother 0.8.27 → 0.8.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/cli.js +78 -0
- package/src/tools.js +12 -1
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -606,6 +606,50 @@ function buildSyntheticAssistantOutput(receipt) {
|
|
|
606
606
|
return null;
|
|
607
607
|
}
|
|
608
608
|
|
|
609
|
+
function hasFrontendCodeEcho(text) {
|
|
610
|
+
const body = String(text || "");
|
|
611
|
+
return /```(?:html|css|js|javascript|jsx|tsx)?[\s\S]{120,}```/i.test(body) || /<!DOCTYPE html>/i.test(body);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function hasWriteMutationTool(receipt) {
|
|
615
|
+
const tools = Array.isArray(receipt?.tools) ? receipt.tools : [];
|
|
616
|
+
return tools.some((tool) => ["write_file", "replace_in_file", "apply_patch"].includes(tool?.name));
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function deriveContractWriteTarget(contract) {
|
|
620
|
+
const paths = Array.isArray(contract?.paths) ? contract.paths : [];
|
|
621
|
+
for (const item of paths) {
|
|
622
|
+
const raw = String(item || "").trim();
|
|
623
|
+
if (!raw) continue;
|
|
624
|
+
const normalized = raw.replace(/\/\*\*?$/g, "").replace(/\*+$/g, "");
|
|
625
|
+
if (normalized) return normalized;
|
|
626
|
+
}
|
|
627
|
+
return null;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function shouldRecoverFrontendCodeEcho({ frontendExecutionContext, receipt, assistantText }) {
|
|
631
|
+
if (!frontendExecutionContext?.frontend) return false;
|
|
632
|
+
if (!receipt || receipt.mutated) return false;
|
|
633
|
+
if (!hasFrontendCodeEcho(assistantText)) return false;
|
|
634
|
+
if (hasWriteMutationTool(receipt)) return false;
|
|
635
|
+
const tools = Array.isArray(receipt.tools) ? receipt.tools : [];
|
|
636
|
+
return tools.some((tool) => tool?.name === "declare_contract") || tools.some((tool) => tool?.name === "make_directory");
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function buildFrontendWriteRecoveryPrompt({ originalPrompt, contract }) {
|
|
640
|
+
const target = deriveContractWriteTarget(contract);
|
|
641
|
+
const lines = [
|
|
642
|
+
"You already planned the work but printed code into chat instead of writing files.",
|
|
643
|
+
`Original task: ${String(originalPrompt || "").trim()}`,
|
|
644
|
+
target ? `Write the generated frontend into the declared scope now: ${target}` : "Write the generated frontend into the declared contract scope now.",
|
|
645
|
+
"Do not print long code blocks in chat.",
|
|
646
|
+
"Use write_file (or replace_in_file/apply_patch if needed) to create the actual site files.",
|
|
647
|
+
"If this is a new site in a folder, default to writing index.html there unless multiple files are clearly justified.",
|
|
648
|
+
"After writing, reply briefly with only the files created or updated."
|
|
649
|
+
];
|
|
650
|
+
return lines.join("\n\n");
|
|
651
|
+
}
|
|
652
|
+
|
|
609
653
|
function color256(fg, text) {
|
|
610
654
|
return `\x1b[38;5;${fg}m${text}\x1b[0m`;
|
|
611
655
|
}
|
|
@@ -3576,6 +3620,40 @@ async function runTextTurnInteractive({
|
|
|
3576
3620
|
precomputedReceipt = await agent.toolRuntime.completeTurn({ signal: abortController?.signal });
|
|
3577
3621
|
renderedAssistantText = buildSyntheticAssistantOutput(precomputedReceipt) || renderedAssistantText;
|
|
3578
3622
|
}
|
|
3623
|
+
if (!precomputedReceipt && frontendExecutionContext) {
|
|
3624
|
+
const candidateReceipt = await agent.toolRuntime.completeTurn({ signal: abortController?.signal });
|
|
3625
|
+
if (shouldRecoverFrontendCodeEcho({ frontendExecutionContext, receipt: candidateReceipt, assistantText: response.content || "" })) {
|
|
3626
|
+
const recoverySpinner = createProgressSpinner("writing files...");
|
|
3627
|
+
printLiveTrace("frontend recovery: assistant echoed code, retrying with write_file", context.runtime.traceMode);
|
|
3628
|
+
agent.toolRuntime.setReadOnlyRoots(readOnlyRoots);
|
|
3629
|
+
agent.toolRuntime.setWriteRoots(writeRoots);
|
|
3630
|
+
if (frontendExecutionContext) {
|
|
3631
|
+
const merged = { ...(previousExecutionContext || {}), ...frontendExecutionContext };
|
|
3632
|
+
if (previousExecutionContext?.reminders && frontendExecutionContext.reminders) {
|
|
3633
|
+
merged.reminders = `${previousExecutionContext.reminders}\n${frontendExecutionContext.reminders}`;
|
|
3634
|
+
}
|
|
3635
|
+
agent.setExecutionContext(merged);
|
|
3636
|
+
}
|
|
3637
|
+
try {
|
|
3638
|
+
response = await agent.runTurn(buildFrontendWriteRecoveryPrompt({ originalPrompt: effectivePromptText, contract: candidateReceipt?.contract }), {
|
|
3639
|
+
signal: abortController?.signal,
|
|
3640
|
+
onStateChange(state) {
|
|
3641
|
+
printLiveTrace(`state=${state}`, context.runtime.traceMode, { verboseOnly: true });
|
|
3642
|
+
}
|
|
3643
|
+
});
|
|
3644
|
+
renderedAssistantText = response.content || "";
|
|
3645
|
+
} finally {
|
|
3646
|
+
recoverySpinner.stop();
|
|
3647
|
+
agent.toolRuntime.setReadOnlyRoots([]);
|
|
3648
|
+
agent.toolRuntime.setWriteRoots([]);
|
|
3649
|
+
if (frontendExecutionContext) {
|
|
3650
|
+
agent.setExecutionContext(previousExecutionContext);
|
|
3651
|
+
}
|
|
3652
|
+
}
|
|
3653
|
+
} else {
|
|
3654
|
+
precomputedReceipt = candidateReceipt;
|
|
3655
|
+
}
|
|
3656
|
+
}
|
|
3579
3657
|
printAssistantOutput(renderedAssistantText);
|
|
3580
3658
|
await setSessionRunState(currentSession, agent, "done");
|
|
3581
3659
|
printTurnSummary(turnSummary, response, { modelId: agent.getModel(), costTracker: context.costTracker, traceMode: context.runtime.traceMode });
|
package/src/tools.js
CHANGED
|
@@ -1037,12 +1037,23 @@ export function createToolRuntime({
|
|
|
1037
1037
|
return { decision: "ask", reason: "No matching allow rule" };
|
|
1038
1038
|
}
|
|
1039
1039
|
|
|
1040
|
+
function getContractWriteRoots() {
|
|
1041
|
+
const rawPaths = Array.isArray(currentTurn.contract?.paths) ? currentTurn.contract.paths : [];
|
|
1042
|
+
return normalizePathList(
|
|
1043
|
+
rawPaths
|
|
1044
|
+
.map((item) => String(item || "").trim())
|
|
1045
|
+
.filter(Boolean)
|
|
1046
|
+
.map((item) => item.replace(/\/\*\*?$/g, "").replace(/\*+$/g, ""))
|
|
1047
|
+
.filter(Boolean)
|
|
1048
|
+
);
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1040
1051
|
function contractAllows(toolName, args = {}) {
|
|
1041
1052
|
if (!currentTurn.contract) return { ok: !toolRequiresContract(toolName, args), reason: "No active contract" };
|
|
1042
1053
|
if ((toolName === "write_file" || toolName === "replace_in_file" || toolName === "make_directory" || toolName === "delete_path") && args.path) {
|
|
1043
1054
|
try {
|
|
1044
1055
|
resolveSandboxPath(cwd, args.path, allowOutsideCwd, {
|
|
1045
|
-
allowedWriteRoots: getCurrentWriteRoots()
|
|
1056
|
+
allowedWriteRoots: [...getCurrentWriteRoots(), ...getContractWriteRoots()]
|
|
1046
1057
|
});
|
|
1047
1058
|
} catch {
|
|
1048
1059
|
const touchedPaths = getTouchedPathsForTool(toolName, args);
|