diff --git a/.claude/skills/changelog/SKILL.md b/.claude/skills/changelog/SKILL.md index 5c15ece..281bbe9 100644 --- a/.claude/skills/changelog/SKILL.md +++ b/.claude/skills/changelog/SKILL.md @@ -18,10 +18,12 @@ Editing the file alone changes nothing live — steps 2 and 3 are required. ## Entry format -Each entry is a 4-element array: `[type, summary, detail, 'YYYY-MM-DD HH:MM']` +Each entry is an array of `[type, summary, detail, 'YYYY-MM-DD HH:MM', by?]` — the 5th +element (**by**, the actor) is optional: ```php ['added', 'Contact form wired up', 'Real submissions stored in the database, with validation and spam protection.', '2026-07-23 12:14'], +['updated', 'Hero spacing tightened', 'Trimmed the vertical rhythm on the homepage hero.', '2026-07-24 09:00', 'Website Designer Agent'], ``` - **type** — one of: `added`, `updated`, `fixed`, `bug`, `removed`, `note`. @@ -33,6 +35,14 @@ Each entry is a 4-element array: `[type, summary, detail, 'YYYY-MM-DD HH:MM']` orders the timeline** — the page shows entries newest-first by this value, not by array position — so a new entry can go anywhere in the array as long as its timestamp is right. Use the real date/time the change happened; if unknown, use now. +- **by** (optional, 5th element) — **who made the change.** The changelog shows it as + "Added · 09:00 · by …". Attribute it to whoever actually did the work: + - An **agent** attributes the entry to **itself** — the Web Designer agent uses + `'Website Designer Agent'`. + - A change made by the site owner (e.g. directed through the Claude CLI) is + `'Carlos Arias'`, which is also the **default** when the 5th element is omitted. + - Only name an agent when that agent genuinely made the change. If the Web Designer didn't + do it, it's `'Carlos Arias'`. ## Voice diff --git a/agents/console/server.mjs b/agents/console/server.mjs index 701bf88..9226d78 100644 --- a/agents/console/server.mjs +++ b/agents/console/server.mjs @@ -435,6 +435,12 @@ function liveUrl(target) { return target; } +function nowStamp() { + const d = new Date(); + const p = (n) => String(n).padStart(2, "0"); + return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`; +} + function buildDesignPrompt(task) { let assets = { images: [], videos: [] }; try { assets = JSON.parse(task.assets || "{}") || {}; } catch {} @@ -484,7 +490,17 @@ function buildDesignPrompt(task) { for (const v of videos) p.push(`- ${v}`); p.push(""); } - p.push("Keep the change scoped to what's asked and leave the working tree with only your intended edits."); + p.push( + "", + "After you finish the change, log it to the public changelog using your `changelog` skill:", + "- Add ONE entry to the $entries array in api/cli/seed-changelog.php, in the site's visitor-facing voice.", + `- Use the timestamp '${nowStamp()}' and attribute it to 'Website Designer Agent' (the 5th array element).`, + "- Choose the right type (added / updated / fixed / removed).", + "- Do NOT run the reseed or the build — the console does that for you.", + "- If you ended up making no change to the site, do not add a changelog entry.", + "", + "Keep the change scoped to what's asked and leave the working tree with only your intended edits.", + ); return p.join("\n"); } @@ -529,6 +545,13 @@ async function runDesignTask(task) { return resolve({ ok: true, commit: "", summary: summary || "No changes were needed.", live: liveUrl(task.target_page) }); } + // The agent logs to the changelog by editing api/cli/seed-changelog.php + // (it has no shell). If it did, reseed cja_changelog so the build picks up + // the new entry — the changelog page reads the DB at build time. + if (/seed-changelog\.php/.test(status.tail)) { + await phpCli(["api/cli/seed-changelog.php"]); + } + // Build must pass before anything is committed. const built = await build({}); if (!built.ok) { diff --git a/api/cli/seed-changelog.php b/api/cli/seed-changelog.php index b3ce649..02fec9e 100644 --- a/api/cli/seed-changelog.php +++ b/api/cli/seed-changelog.php @@ -53,12 +53,17 @@ $entries = [ Db::execute('TRUNCATE TABLE cja_changelog'); +// Each entry is [type, summary, detail, 'YYYY-MM-DD HH:MM', by?]. The 5th +// element (who made the change) is optional and defaults to the site owner. $n = 0; -foreach ($entries as [$type, $summary, $detail, $at]) { +foreach ($entries as $e) { + [$type, $summary, $detail, $at] = $e; + $by = $e[4] ?? 'Carlos Arias'; Db::insert('cja_changelog', [ 'type' => $type, 'summary' => $summary, 'detail' => $detail, + 'actor' => $by, 'entry_at' => $at . ':00', 'published' => 1, ]); diff --git a/api/db/migrations/012_changelog_actor.sql b/api/db/migrations/012_changelog_actor.sql new file mode 100644 index 0000000..e4bb91f --- /dev/null +++ b/api/db/migrations/012_changelog_actor.sql @@ -0,0 +1,7 @@ +-- Attribution for changelog entries: who made the change. +-- +-- e.g. "Added · 00:40 · by Website Designer Agent". Blank = unattributed (the +-- display simply omits the "by …" clause). Seeded from api/cli/seed-changelog.php. + +ALTER TABLE `cja_changelog` + ADD COLUMN `actor` varchar(80) NOT NULL DEFAULT '' AFTER `detail`; diff --git a/app/src/lib/changelog.js b/app/src/lib/changelog.js index 6ff584a..8a9364e 100644 --- a/app/src/lib/changelog.js +++ b/app/src/lib/changelog.js @@ -28,7 +28,7 @@ export async function getChangelog() { try { conn = await mysql.createConnection({ host, user, password, database }); const [rows] = await conn.execute( - `SELECT type, summary, detail, + `SELECT type, summary, detail, actor, DATE_FORMAT(entry_at, '%Y-%m-%dT%H:%i') AS entry_at FROM cja_changelog WHERE published = 1 diff --git a/app/src/pages/changelog.astro b/app/src/pages/changelog.astro index c7e243d..3c7e0ff 100644 --- a/app/src/pages/changelog.astro +++ b/app/src/pages/changelog.astro @@ -67,6 +67,7 @@ const jsonLd = { + {e.actor && by {e.actor}}

{e.summary}

{e.detail &&

{e.detail}

} diff --git a/app/src/styles.css b/app/src/styles.css index eab0e5e..da72300 100644 --- a/app/src/styles.css +++ b/app/src/styles.css @@ -1008,6 +1008,18 @@ font-variant-numeric: tabular-nums; } + .ca-log-by { + font-family: var(--font-mono); + font-size: 0.62rem; + letter-spacing: 0.04em; + color: var(--ca-ink-4); + } + .ca-log-by::before { + content: "·"; + margin-right: 0.5rem; + color: var(--ca-rule); + } + .ca-log-summary { margin: 0; font-size: 0.95rem;