From 233fb6d00343a7af7787157534f5c954bbddb5b2 Mon Sep 17 00:00:00 2001 From: "khalid@traclabs.com" Date: Thu, 23 Apr 2026 07:58:35 -0500 Subject: [PATCH] Autoapply edits if env variable configured --- .env.example | 2 + server/src/llm/client.ts | 59 ++++++++++++++++++++++------ server/src/queue/process-edit-job.ts | 25 ++++++++++-- server/src/sms/templates.ts | 3 ++ 4 files changed, 75 insertions(+), 14 deletions(-) diff --git a/.env.example b/.env.example index ffc7876..10a104f 100644 --- a/.env.example +++ b/.env.example @@ -34,6 +34,8 @@ LOG_LEVEL=debug # Proposals PROPOSAL_TTL_MS=900000 +# Set to "true" to skip the YES/NO confirmation step and apply edits immediately +AUTO_APPLY_EDITS=false # Editor auth EDITOR_SESSION_SECRET=change-me-to-another-random-string diff --git a/server/src/llm/client.ts b/server/src/llm/client.ts index 8a97504..459a1b5 100644 --- a/server/src/llm/client.ts +++ b/server/src/llm/client.ts @@ -242,31 +242,68 @@ export async function generateSummary(params: { after: unknown; repoRelativePath: string; userMessage: string; + autoApply?: boolean; chat?: LlmChatCaller; }): Promise { const chat = params.chat || ollamaChat; - const messages = [ - { - role: 'system', - content: `You write short, friendly summaries of website changes for a small business owner who manages their site via text message. + + const proposalPrompt = `You write short, friendly summaries of PROPOSED website changes for a small business owner who manages their site via text message. + +IMPORTANT: The change has NOT been applied yet. This is a proposal the owner must approve. Your summary will be shown to the owner prefixed with "I'd like to: ", so write it as a proposed action — what WILL happen if they approve. Rules: - Keep it under 140 characters. - Describe what visitors will SEE on the website, not what changed in the data. - Use plain, everyday language. No technical terms, no field names, no file paths, no JSON jargon. -- Write in a warm, conversational tone — like texting a friend. +- Write as a proposed future action — use phrasing like "show…", "add…", "update…", "hide…". +- NEVER use past tense or phrases like "is now live", "has been updated", "done", "all set". Good examples: -- "Show the promo banner on your site" -- "Update your main headline to 'Welcome Home'" -- "Hide the testimonials section from visitors" -- "Add a new event: Wine Tasting on June 15" -- "Change the About section text" +- "show the promo banner on your site" +- "update your main headline to 'Welcome Home'" +- "hide the testimonials section from visitors" +- "add a new event: Wine Tasting on June 15" +- "change the About section text" + +Bad examples (sounds already done — never do this): +- "The promo banner is now live on your site!" +- "Your headline has been updated!" +- "You got it! The sale banner is showing" +- "Done! Testimonials are hidden" Bad examples (too technical — never do this): - "Change promo-banner visible from false to true" - "Update hero.headline field" -- "Modify content/sections/about.json"`, +- "Modify content/sections/about.json"`; + + const autoApplyPrompt = `You write short, friendly summaries of website changes for a small business owner who manages their site via text message. + +IMPORTANT: The change has ALREADY been applied. Your summary will be shown prefixed with "Done! I went ahead and ", so write it as a completed past-tense action. + +Rules: +- Keep it under 140 characters. +- Describe what visitors will NOW SEE on the website, not what changed in the data. +- Use plain, everyday language. No technical terms, no field names, no file paths, no JSON jargon. +- Write in past tense — use phrasing like "updated…", "added…", "showed…", "hid…". +- Start lowercase (the sentence prefix is already provided). +- NEVER use future tense or proposal phrasing like "I'd like to", "will show", "would add". + +Good examples: +- "showed the promo banner on your site" +- "updated your main headline to 'Welcome Home'" +- "hid the testimonials section from visitors" +- "added a new event: Wine Tasting on June 15" +- "changed the About section text" + +Bad examples (too technical — never do this): +- "Change promo-banner visible from false to true" +- "Update hero.headline field" +- "Modify content/sections/about.json"`; + + const messages = [ + { + role: 'system', + content: params.autoApply ? autoApplyPrompt : proposalPrompt, }, { role: 'user', diff --git a/server/src/queue/process-edit-job.ts b/server/src/queue/process-edit-job.ts index 661d831..58c5dff 100644 --- a/server/src/queue/process-edit-job.ts +++ b/server/src/queue/process-edit-job.ts @@ -12,6 +12,7 @@ import { SMS_TEMPLATES } from '../sms/templates.js'; import { logger } from '../logger.js'; const REPO_ROOT = process.env.REPO_ROOT || '.'; +const AUTO_APPLY = process.env.AUTO_APPLY_EDITS === 'true'; /** Get a friendly display name for a manifest entry. */ function sectionDisplayName(m: ManifestEntry): string { @@ -136,6 +137,7 @@ async function handlePropose(job: Extract) after: editedJson, repoRelativePath, userMessage: job.message, + autoApply: AUTO_APPLY, }); // Step 5: Store proposal @@ -154,9 +156,26 @@ async function handlePropose(job: Extract) log.info({ event: 'proposal.created', proposalId, path: repoRelativePath }, 'Proposal created'); - // Step 6: Notify user - if (job.smsReplyMeta) { - await sendSms(job.smsReplyMeta.from, job.smsReplyMeta.to, SMS_TEMPLATES.PROPOSAL_SUMMARY(summary, proposalId)); + // Step 6: Auto-apply or ask for confirmation + if (AUTO_APPLY) { + const validation = schema.safeParse(editedJson); + if (!validation.success) { + log.error({ event: 'auto_apply.validation_failed', errors: validation.error.message }, 'Auto-apply validation failed'); + updateProposalStatus(proposalId, 'rejected'); + return; + } + + writeContentFile(repoRelativePath, validation.data, { proposalId, source: job.source }); + updateProposalStatus(proposalId, 'applied'); + log.info({ event: 'proposal.auto_applied', proposalId, path: repoRelativePath }, 'Proposal auto-applied'); + + if (job.smsReplyMeta) { + await sendSms(job.smsReplyMeta.from, job.smsReplyMeta.to, SMS_TEMPLATES.AUTO_APPLIED(summary)); + } + } else { + if (job.smsReplyMeta) { + await sendSms(job.smsReplyMeta.from, job.smsReplyMeta.to, SMS_TEMPLATES.PROPOSAL_SUMMARY(summary, proposalId)); + } } } catch (err) { diff --git a/server/src/sms/templates.ts b/server/src/sms/templates.ts index 5553072..d502fdb 100644 --- a/server/src/sms/templates.ts +++ b/server/src/sms/templates.ts @@ -5,6 +5,9 @@ export const SMS_TEMPLATES = { APPLIED: (summary: string) => `Done! ${summary} Your site will update in a moment.`, + AUTO_APPLIED: (summary: string) => + `Done! I went ahead and ${summary} Your site will update in a moment.`, + REJECTED: () => `No problem — I cancelled that change. Just text me whenever you'd like to make an edit!`,