CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / TypeScript typecheck (push) Successful in 52s
CI & Build / integration (push) Successful in 1m1s
CI & Build / Python tests (push) Successful in 1m55s
CI & Build / Build & push image (push) Successful in 36s
Step 1 of the Scribe -> Grimoire rename: every capitalised "Scribe" in prose
becomes "Grimoire" ("Fabled Scribe" -> "Fabled Grimoire") across the UI,
the PWA manifest and page title, the MCP instructions and tool docstrings,
hook and skill text, docs and README, with the tests that assert on that
prose moving with it. Old migrations keep their wording.
Identifiers are untouched here: the package, plugin name, MCP server key,
hook file names, env vars, the .scribe marker and FabledScribe repo/image
names move in steps 2 and 3, along with prose that quotes them.
The sweep matched "Scribe" only as a whole word, so describe/subscribe and
friends are untouched. The longer name took the MCP instructions one
character over their 1,600 budget; two phrases are shortened to fit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
197 lines
8.0 KiB
Vue
197 lines
8.0 KiB
Vue
<script setup lang="ts">
|
|
/**
|
|
* What Grimoire has changed about how it works with you.
|
|
*
|
|
* THE RISK THIS EXISTS FOR. A preference is the one record
|
|
* kind the agent rewrites on its own, mid-work, without asking — which is
|
|
* what keeps it current and what makes it dangerous. An agent misreads one
|
|
* session, rewrites a preference, and follows the rewritten version forever
|
|
* while the operator never sees the moment it changed. That is worse than
|
|
* having no preference at all: a confident wrong answer wearing the
|
|
* operator's own authority.
|
|
*
|
|
* `rule_versions` already recorded every rewrite. What it could not do is
|
|
* ARRIVE. A history you open one rule at a time, having first suspected that
|
|
* rule, is not oversight — so this pane is the PUSH half, and it sits beside
|
|
* the staleness sweep for the same reason that does: drift belongs to no one
|
|
* rulebook.
|
|
*
|
|
* Cross-cutting, and deliberately not a filter on the per-topic rule list —
|
|
* that list shows one topic of one rulebook, so filtering it would silently
|
|
* under-report, which is the exact failure this surface exists to catch.
|
|
*/
|
|
import { onMounted, ref } from "vue";
|
|
import DiffView from "@/components/DiffView.vue";
|
|
import { computeDiff } from "@/utils/diff";
|
|
import { useRulebooksStore } from "@/stores/rulebooks";
|
|
import { useToastStore } from "@/stores/toast";
|
|
import type { PreferenceDrift } from "@/api/rulebooks";
|
|
|
|
const emit = defineEmits<{ "open-rule": [id: number] }>();
|
|
|
|
const store = useRulebooksStore();
|
|
const toast = useToastStore();
|
|
const openId = ref<number | null>(null);
|
|
const busyId = ref<number | null>(null);
|
|
|
|
/** Old on the left, new on the right — the direction a reader expects of
|
|
* "what changed", and the opposite of the rule history panel, which is
|
|
* answering "what did it used to say" from the current text backwards. */
|
|
function diffFor(row: PreferenceDrift) {
|
|
return computeDiff(row.previous.statement, row.current.statement);
|
|
}
|
|
|
|
function triggerChanged(row: PreferenceDrift): boolean {
|
|
return row.previous.when_to_apply !== row.current.when_to_apply;
|
|
}
|
|
|
|
function stamp(iso: string | null): string {
|
|
return iso ? iso.slice(0, 10) : "";
|
|
}
|
|
|
|
function toggle(row: PreferenceDrift) {
|
|
openId.value = openId.value === row.rule.id ? null : row.rule.id;
|
|
}
|
|
|
|
/** The veto. One action, because a veto that costs more than shrugging is
|
|
* not really supervision — and nothing is lost either way: the restore is
|
|
* itself an edit, so the rewrite stays in the preference's history with the
|
|
* revert recorded after it. */
|
|
async function restore(row: PreferenceDrift) {
|
|
busyId.value = row.rule.id;
|
|
try {
|
|
await store.restoreVersion(row.rule.id, row.previous.id);
|
|
toast.show(`Put “${row.previous.title}” back`, "success");
|
|
openId.value = null;
|
|
} catch {
|
|
toast.show("Could not put that wording back", "error");
|
|
} finally {
|
|
busyId.value = null;
|
|
}
|
|
}
|
|
|
|
onMounted(() => store.fetchDrift());
|
|
</script>
|
|
|
|
<template>
|
|
<section class="pane drift">
|
|
<header>
|
|
<h2>Recent changes</h2>
|
|
<p class="sweep-lede">
|
|
Preferences Grimoire rewrote while working, most recently changed first. A
|
|
preference is how you want work done, so sessions keep it current
|
|
without asking — this is where you see what they decided. Rules are not
|
|
here: those change when you change them.
|
|
</p>
|
|
</header>
|
|
|
|
<p v-if="store.loading" class="sweep-state">Loading…</p>
|
|
|
|
<!-- Nothing changed is the ordinary state and must not read as a fault. -->
|
|
<p v-else-if="!store.drift.length" class="sweep-state empty">
|
|
Nothing has been rewritten. A preference appears here the first time a
|
|
session changes one — until then there is nothing to review.
|
|
</p>
|
|
|
|
<ol v-else class="sweep-rows">
|
|
<li v-for="row in store.drift" :key="row.rule.id" class="sweep-row">
|
|
<div class="sweep-row-head">
|
|
<button class="sweep-row-title" @click="emit('open-rule', row.rule.id)">
|
|
{{ row.rule.title }}
|
|
</button>
|
|
<span class="when">{{ stamp(row.previous.created_at) }}</span>
|
|
</div>
|
|
|
|
<!-- The provenance, named rather than numbered: a bare id reads as
|
|
complete to the writer and as homework to the reader. -->
|
|
<p v-if="row.taught_by" class="taught">
|
|
Learned from <em>{{ row.taught_by.title }}</em>
|
|
<span class="taught-id">#{{ row.taught_by.id }}</span>
|
|
</p>
|
|
<p v-else class="taught untaught">
|
|
Nothing recorded what taught this change.
|
|
</p>
|
|
|
|
<button class="expand" :aria-expanded="openId === row.rule.id" @click="toggle(row)">
|
|
{{ openId === row.rule.id ? "Hide what changed" : "See what changed" }}
|
|
</button>
|
|
|
|
<div v-if="openId === row.rule.id" class="detail">
|
|
<p v-if="triggerChanged(row)" class="trigger-moved">
|
|
Its trigger changed too, so it now arrives at a different moment.
|
|
<span class="was">Was:</span> {{ row.previous.when_to_apply || "nothing" }}
|
|
</p>
|
|
<DiffView v-if="diffFor(row).length" :diff="diffFor(row)" />
|
|
<p v-else class="sweep-state">
|
|
The statement is unchanged — this edit moved another field.
|
|
</p>
|
|
<div class="sweep-actions">
|
|
<button
|
|
:disabled="busyId === row.rule.id"
|
|
@click="restore(row)"
|
|
>Put the old wording back</button>
|
|
<span class="actions-note">
|
|
Kept, not erased: this is recorded as another edit, so both
|
|
wordings stay in the preference's history.
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</li>
|
|
</ol>
|
|
|
|
<p v-if="store.drift.length" class="footnote">
|
|
One row per preference, carrying its latest rewrite. A preference changed
|
|
several times shows the most recent one — its full history is in its
|
|
editor, under <strong>Edit history</strong>.
|
|
</p>
|
|
</section>
|
|
</template>
|
|
|
|
<style src="@/assets/rules-shared.css" />
|
|
<style scoped>
|
|
.drift { display: flex; flex-direction: column; gap: var(--fs-space-3); }
|
|
.when {
|
|
margin-left: auto; font-size: var(--fs-size-tiny);
|
|
color: var(--fs-text-secondary); font-variant-numeric: tabular-nums;
|
|
}
|
|
|
|
.taught {
|
|
margin: var(--fs-space-2) 0 0; font-size: var(--fs-size-tiny);
|
|
color: var(--fs-text-secondary); line-height: var(--fs-leading-body);
|
|
}
|
|
.taught em { font-style: italic; color: var(--fs-text-primary); }
|
|
.taught-id { margin-left: 0.35rem; color: var(--fs-text-tertiary); font-variant-numeric: tabular-nums; }
|
|
/* Not a warning: every preference written before provenance was required has
|
|
none, and marking those as faults would cry wolf on the whole backlog. */
|
|
.taught.untaught { color: var(--fs-text-tertiary); font-style: italic; }
|
|
|
|
.expand {
|
|
align-self: flex-start; margin-top: var(--fs-space-2);
|
|
background: none; border: none; padding: 0; cursor: pointer;
|
|
font: inherit; font-size: var(--fs-size-tiny); color: var(--fs-text-secondary);
|
|
}
|
|
.expand:hover { color: var(--fs-text-primary); text-decoration: underline; }
|
|
|
|
.detail { margin-top: var(--fs-space-2); display: flex; flex-direction: column; gap: var(--fs-space-2); }
|
|
/* A TINT, not the solid token — `--fs-warning-fg` is defined as warning text
|
|
ON a warning tint, and painting it over solid `--fs-warning` is the
|
|
same-hue contrast failure #3141 records. */
|
|
.trigger-moved {
|
|
margin: 0; font-size: var(--fs-size-tiny); line-height: var(--fs-leading-body);
|
|
color: var(--fs-warning-fg);
|
|
background: color-mix(in srgb, var(--fs-warning) 12%, transparent);
|
|
border-radius: var(--fs-radius-sm); padding: var(--fs-space-2);
|
|
}
|
|
.was { color: var(--fs-text-tertiary); }
|
|
|
|
.actions-note {
|
|
flex: 1; min-width: 18ch; font-size: var(--fs-size-tiny);
|
|
color: var(--fs-text-tertiary); line-height: var(--fs-leading-body);
|
|
}
|
|
|
|
.footnote { margin: 0; max-width: 62ch; font-size: var(--fs-size-tiny); color: var(--fs-text-tertiary); line-height: var(--fs-leading-body); }
|
|
/* Drift's buttons sit beside a note inside the expanded detail, not under a
|
|
row of facts. */
|
|
.sweep-actions { align-items: baseline; gap: var(--fs-space-3); flex-wrap: wrap; margin-top: 0; }
|
|
</style>
|