changelog: 'by' attribution + Web Designer self-logs via the skill

- migration 012: cja_changelog.actor column
- seed-changelog: optional 5th 'by' element (defaults to Carlos Arias);
  display shows 'Added · time · by <actor>'
- changelog skill documents the actor field + attribution logic (agents name
  themselves, human/CLI edits are Carlos Arias)
- Web Designer prompt now logs each change to the changelog via the skill,
  attributed to 'Website Designer Agent'; runner reseeds cja_changelog when the
  seed file changed (the agent has no shell)
This commit is contained in:
Carlos Arias 2026-07-24 02:45:53 +00:00
parent 2ad551dc5f
commit df5e5f9456
7 changed files with 62 additions and 4 deletions

View file

@ -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

View file

@ -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) {

View file

@ -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,
]);

View file

@ -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`;

View file

@ -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

View file

@ -67,6 +67,7 @@ const jsonLd = {
<time class="ca-log-time" datetime={e.entry_at}>
{time(e.entry_at)}
</time>
{e.actor && <span class="ca-log-by">by {e.actor}</span>}
</div>
<p class="ca-log-summary">{e.summary}</p>
{e.detail && <p class="ca-log-detail">{e.detail}</p>}

View file

@ -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;