docs(6210): acceptance sweep — src, scripts and frontend leftovers described in the present tense
CI & Build / Python lint (push) Successful in 2s
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / TypeScript typecheck (push) Successful in 53s
CI & Build / integration (push) Successful in 1m9s
CI & Build / Python tests (push) Successful in 1m55s
CI & Build / Build & push image (push) Successful in 34s
CI & Build / Python lint (push) Successful in 2s
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / TypeScript typecheck (push) Successful in 53s
CI & Build / integration (push) Successful in 1m9s
CI & Build / Python tests (push) Successful in 1m55s
CI & Build / Build & push image (push) Successful in 34s
The acceptance greps found what the per-area passes missed: - src/scribe: "Phase 7/8" removal notes in projects, settings and user_profile; a paragraph promising an auto-summary side effect that no longer exists; user_profile's claim that "upcoming MCP profile tools" read the profile, which nothing does; an instance count and instance rule titles in plugin_context; "MILESTONE 323 DECIDED" in rulebooks. - scripts/: check_plugin's header and inline comments were a history of the incidents behind each guard, with issue numbers in two failure messages; check_dangling_styles named this app's classes as its examples; check_design_tokens, measure_duplication and mint_plugin_version cited issues and checklists. - frontend/src: forty-odd comments with four-digit issue numbers that the earlier frontend scan filtered out as hex colours, plus "used to" and "Phase N" notes. Kept: illustrative ids (`#4181` as a bare-id example, `PR #198`), migration Create Date headers and one migration's dated data floor, and the plugin skills' worked examples of how to cite a record. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -13,11 +13,10 @@ import { fetchVersion } from "@/api/version";
|
||||
useTheme();
|
||||
|
||||
const router = useRouter();
|
||||
// THREE states, not two (#3127 checklist 12). `null` is "not answered yet" and
|
||||
// renders nothing; a string renders; `appVersionFailed` renders its own thing.
|
||||
// This used to default to the literal "dev" and swallow the error, which meant
|
||||
// an instance that could not answer was indistinguishable from a local build
|
||||
// that genuinely reports "dev" — a blank standing in for `unknown`, in the one
|
||||
// THREE states, not two. `null` is "not answered yet" and renders nothing; a
|
||||
// string renders; `appVersionFailed` renders its own thing. Defaulting to the
|
||||
// literal "dev" and swallowing the error would make an instance that cannot
|
||||
// answer indistinguishable from a local build that genuinely reports "dev" — a blank standing in for `unknown`, in the one
|
||||
// readout whose whole job is to say what is running.
|
||||
const appVersion = ref<string | null>(null);
|
||||
const appVersionFailed = ref(false);
|
||||
|
||||
@@ -41,8 +41,8 @@ async function handleResponse<T>(res: Response, path: string): Promise<T> {
|
||||
/**
|
||||
* The server's `{"error": "..."}` message from a failed call, or `fallback`
|
||||
* when the failure carried none (network error, non-JSON body). The one place
|
||||
* the error envelope is unpacked on the client — views used to restate this
|
||||
* as a six-line `"body" in e` branch at every catch site.
|
||||
* the error envelope is unpacked on the client, so no view restates it as a
|
||||
* `"body" in e` branch at its catch site.
|
||||
*/
|
||||
export function apiErrorMessage(e: unknown, fallback: string): string {
|
||||
if (e && typeof e === "object" && "body" in e) {
|
||||
@@ -61,8 +61,7 @@ export function apiErrorMessage(e: unknown, fallback: string): string {
|
||||
* surface can show for "pending forever" that is not a lie.
|
||||
*
|
||||
* 30s is chosen to be longer than anything healthy: it has to clear a cold
|
||||
* embedding call and a list view under connection-pool contention (#2384 had
|
||||
* /api/projects fanning 25 concurrent sessions at a 15-connection pool), so
|
||||
* embedding call and a list view under connection-pool contention, so
|
||||
* tripping it means something is genuinely wrong rather than merely busy. Slow
|
||||
* BY DESIGN is a different case and passes its own value — see the callers in
|
||||
* SettingsView that do.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Design systems — the stylesheet held as records (milestone #254).
|
||||
* Design systems — the stylesheet held as records.
|
||||
*
|
||||
* A design system is a named set of tokens with an optional parent. A system
|
||||
* with no parent is a "family"; one with a parent holds ONLY what it changes,
|
||||
@@ -82,8 +82,8 @@ export interface StarterRoleGroup {
|
||||
}
|
||||
|
||||
/** The starter token ROLES offered at creation — names and purposes, never
|
||||
* values. A default palette would be one install's taste shipped as product
|
||||
* (rule #115), so the values are always the operator's to fill. */
|
||||
* values. A default palette would be one install's taste shipped as product,
|
||||
* so the values are always the operator's to fill. */
|
||||
export const listStarterRoleGroups = () =>
|
||||
apiGet<{ groups: StarterRoleGroup[]; default_prefix: string }>(
|
||||
"/api/design-systems/starter-roles",
|
||||
|
||||
@@ -88,7 +88,7 @@ export interface Lesson {
|
||||
no_rule?: { why: string; judged_at: string | null };
|
||||
/** Sent by a create or update that recorded "no rule fits" when that answer
|
||||
* completes a group: lessons with no rule that keep landing in one
|
||||
* situation, which may want a rule written for it (#4634). */
|
||||
* situation, which may want a rule written for it. */
|
||||
convergence?: { lessons: { id: number; title: string }[] };
|
||||
/** Set when another user owns this record. */
|
||||
shared?: boolean;
|
||||
|
||||
@@ -30,7 +30,7 @@ export interface SnippetFields {
|
||||
* no `locations`/`tags` predates that attribution and cannot be un-merged. */
|
||||
merged_from: { id: number; locations?: SnippetLocation[]; tags?: string[] }[];
|
||||
code: string;
|
||||
/** Free text that is not the situation — why, history, caveats (#4378).
|
||||
/** Free text that is not the situation — why, history, caveats.
|
||||
* Kept out of `when_to_use`, which the snippet is ranked on. */
|
||||
notes: string;
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ export interface System {
|
||||
open_issue_count: number;
|
||||
/**
|
||||
* How often this area's rulings were shown to a session because its files
|
||||
* were touched (`surfaced_count`), and how often it was opened (#4769).
|
||||
* were touched (`surfaced_count`), and how often it was opened.
|
||||
* Present on the list and detail reads; optional because a System returned
|
||||
* by a create or update carries none.
|
||||
*/
|
||||
@@ -45,7 +45,7 @@ export async function listSystems(projectId: number): Promise<System[]> {
|
||||
* caller to accept or ignore.
|
||||
*
|
||||
* A same-named System in this project is a 409 ApiError carrying
|
||||
* `{duplicate, existing_id}` — the same gate the MCP door enforces (#2482).
|
||||
* `{duplicate, existing_id}` — the same gate the MCP door enforces.
|
||||
*/
|
||||
export interface CreatedSystem extends System {
|
||||
canonical_suggestion?: CanonicalMatch;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* The sweep-row visual language (#3207).
|
||||
/* The sweep-row visual language.
|
||||
*
|
||||
* A "sweep" is a pane that lists records in the order they most need a human
|
||||
* — the note verification sweep, the rule sweep, preference drift — as one
|
||||
|
||||
@@ -132,11 +132,10 @@ router.afterEach(() => {
|
||||
}
|
||||
/* Three tracks, not a flex row with an absolutely-centred overlay.
|
||||
*
|
||||
* The pill bar used to be `position: absolute; left: 50%`, which meant it did
|
||||
* not participate in layout: when the header ran out of room it OVERLAPPED the
|
||||
* brand and the utility cluster rather than pushing them, and nothing wrapped
|
||||
* or scrolled to signal it. A sixth link reached that point at ~1270px, which
|
||||
* is an ordinary window on any monitor.
|
||||
* A pill bar at `position: absolute; left: 50%` does not participate in
|
||||
* layout: when the header runs out of room it OVERLAPS the brand and the
|
||||
* utility cluster rather than pushing them, and nothing wraps or scrolls to
|
||||
* signal it — at an ordinary window width on any monitor.
|
||||
*
|
||||
* `1fr auto 1fr` fixes it structurally. A `1fr` track has an AUTO minimum, so
|
||||
* neither side can be squeezed below its content, and the two side tracks stay
|
||||
@@ -327,7 +326,7 @@ router.afterEach(() => {
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
/* The grid above means running out of room can no longer cause a collision —
|
||||
/* The grid above means running out of room cannot cause a collision —
|
||||
but it can still make the header wider than the window, and a horizontally
|
||||
scrolling header is its own defect. So shed width before that happens. The
|
||||
wordmark goes first: the logo beside it says the same thing and is still the
|
||||
|
||||
@@ -12,8 +12,7 @@
|
||||
* A COMPONENT rather than markup inside the task editor, because the same
|
||||
* question is worth answering on any record a lesson can cite — an issue, a
|
||||
* spike, a dev-log. One panel, mounted wherever that is true, instead of the
|
||||
* shape being written a second time the first time someone wants it on notes
|
||||
* (#3207).
|
||||
* shape being written a second time the first time someone wants it on notes.
|
||||
*
|
||||
* SILENT WHEN EMPTY. Most records taught no lesson, and a panel that renders
|
||||
* "None yet" on every page is a panel people learn to skip — which costs the
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* A project's own code, checked against the design system it is bound to (#2432).
|
||||
* A project's own code, checked against the design system it is bound to.
|
||||
*
|
||||
* This is what the design surface is FOR: a project's recorded components
|
||||
* measured against the sheet they are supposed to use. The check itself is not
|
||||
@@ -38,7 +38,7 @@ async function run() {
|
||||
} catch {
|
||||
// Said out loud rather than rendered as an empty result. "Couldn't check"
|
||||
// and "nothing to report" look identical if you let them, and that is how
|
||||
// a check comes to sit dead without anyone noticing (#2419).
|
||||
// a check comes to sit dead without anyone noticing.
|
||||
failed.value = true;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*
|
||||
* Deliberately not StatusBadge. That component is typed to TaskStatus and
|
||||
* speaks a different vocabulary; these two only ever shared a CSS class name,
|
||||
* which is what made them look like one shape that had drifted (#3132).
|
||||
* which is what makes them look like one shape that has drifted.
|
||||
*
|
||||
* Extracted because ProjectView and ProjectListView really were spelling the
|
||||
* same pill twice, with the differences you get from two hands rather than
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* The starter token ROLES offered when a design system is created (#2349).
|
||||
* The starter token ROLES offered when a design system is created.
|
||||
*
|
||||
* WHY THIS IS A COMPONENT
|
||||
* DesignSystemsView has two creation forms — the empty state and the one inside
|
||||
@@ -11,7 +11,7 @@
|
||||
* WHAT IT OFFERS
|
||||
* Names and purposes, never values. A role is a question the operator answers
|
||||
* with their own palette; a default palette would be one install's taste
|
||||
* shipped as product (rule #115). Every group is individually skippable —
|
||||
* shipped as product. Every group is individually skippable —
|
||||
* an operator who wants three tokens should get three.
|
||||
*
|
||||
* All groups are checked by default. That default lives HERE rather than in the
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { TaskStatus } from "@/types/task";
|
||||
const props = defineProps<{
|
||||
status: TaskStatus;
|
||||
clickable?: boolean;
|
||||
/** Dense surfaces — smaller, unshouted. The canon (#2960) names compact a
|
||||
/** Dense surfaces — smaller, unshouted. The canon names compact a
|
||||
VARIANT of this component rather than a reason to re-spell it. */
|
||||
compact?: boolean;
|
||||
}>();
|
||||
|
||||
@@ -72,7 +72,7 @@ function areaName(system: System): string | null {
|
||||
return canon.byId(system.canonical_id)?.name ?? null;
|
||||
}
|
||||
|
||||
/** How many times this area's rulings reached a session (#4769). Not the
|
||||
/** How many times this area's rulings reached a session. Not the
|
||||
* UsageBadge: its "N/M used" reads opens over showings, and rulings are
|
||||
* delivered in full in the line, so an unopened System is the arm working,
|
||||
* not dead weight. */
|
||||
@@ -721,16 +721,16 @@ async function confirmDelete() {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
/* RESTORED (#2444). Both lost their base rule to a CSS sweep; only the
|
||||
`--archived` modifier and the `:hover .system-actions` reveal survived.
|
||||
The card WAS a flex row and every child still says so — `.system-swatch`
|
||||
/* BASE RULES for the card and the list. Without them only the `--archived`
|
||||
modifier and the `:hover .system-actions` reveal would remain.
|
||||
The card is a flex row and every child says so — `.system-swatch`
|
||||
and `.system-actions` are `flex-shrink: 0`, `.system-body` is `flex: 1`,
|
||||
and `.system-form--inline` is `flex: 1`. `align-items: flex-start` is why
|
||||
the swatch carries `margin-top: 0.3rem`: it is nudged onto the first line
|
||||
of text rather than centred against the whole card.
|
||||
|
||||
The list had no rule at all, so it rendered with browser bullets and
|
||||
indent — invisible to the dangling-style check, which can only see a class
|
||||
Without its rule the list renders with browser bullets and indent —
|
||||
invisible to the dangling-style check, which can only see a class
|
||||
that is PARTLY styled. A class with no rules anywhere looks exactly like a
|
||||
semantic-only hook.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* A design system's tokens, drawn rather than listed (#2431).
|
||||
* A design system's tokens, drawn rather than listed.
|
||||
*
|
||||
* WHAT MAKES THIS WORK FOR A SYSTEM YOU AREN'T RUNNING
|
||||
* Every value is resolved on an offscreen probe carrying only this system's
|
||||
@@ -14,7 +14,7 @@
|
||||
* SPECIMENS ARE CHOSEN BY VALUE SHAPE, NEVER BY NAME
|
||||
* A colour is drawn as a swatch, a length as a rule of that length, a font
|
||||
* stack as text set in it. Nothing here matches `--fs-space-*` or any other
|
||||
* naming convention, because the convention is the install's (rule #115) — a
|
||||
* naming convention, because the convention is the install's — a
|
||||
* system that calls its spacing `--gap-N` gets the same treatment.
|
||||
*
|
||||
* A token with no value for the chosen mode is shown as undecided rather than
|
||||
|
||||
@@ -13,8 +13,8 @@
|
||||
* fields moved".
|
||||
*
|
||||
* What was genuinely shared is shared: DiffView.vue takes DiffLine[] and
|
||||
* nothing note-shaped, and the LCS walk now lives in utils/diff.ts, which
|
||||
* this file uses rather than copying a fourth time (#3207).
|
||||
* nothing note-shaped, and the LCS walk lives in utils/diff.ts, which this
|
||||
* file uses rather than carrying a copy of its own.
|
||||
*/
|
||||
import { computed, onMounted, ref, watch } from "vue";
|
||||
import DiffView from "@/components/DiffView.vue";
|
||||
|
||||
@@ -62,7 +62,7 @@ ul { list-style: none; padding: 0; margin: 1rem 0; }
|
||||
li { padding: 0.5rem; cursor: pointer; border-radius: 6px; }
|
||||
li.active { background: var(--fs-accent-soft); }
|
||||
li:hover { background: var(--fs-surface-hover); }
|
||||
/* `.new-topic` is deliberately bare (#2444): it wraps a button-or-form whose
|
||||
/* `.new-topic` is deliberately bare: it wraps a button-or-form whose
|
||||
children style themselves, and namespaces the descendant rule below. */
|
||||
.new-topic input {
|
||||
width: 100%; margin-bottom: 0.5rem;
|
||||
|
||||
@@ -12,8 +12,7 @@ export function relativeTime(iso: string): string {
|
||||
|
||||
/**
|
||||
* relativeTime() for the recent past, a short date once it's a week old —
|
||||
* the workspace panels' list-row timestamp ("3h ago" / "Jan 15"). Two
|
||||
* panels used to carry identical copies of this.
|
||||
* the workspace panels' list-row timestamp ("3h ago" / "Jan 15").
|
||||
*/
|
||||
export function relativeTimeOrDate(iso: string): string {
|
||||
const d = new Date(iso);
|
||||
|
||||
@@ -7,7 +7,7 @@ import "./assets/theme.css";
|
||||
import "./assets/components.css";
|
||||
import "./assets/prose.css";
|
||||
// Global rather than `<style src>` per pane: plugin-vue cannot compile one
|
||||
// src stylesheet shared by several SFCs (#3207 broke the image build).
|
||||
// src stylesheet shared by several SFCs, and the image build fails on it.
|
||||
import "./assets/sweep-shared.css";
|
||||
|
||||
const app = createApp(App);
|
||||
|
||||
@@ -137,10 +137,9 @@ const router = createRouter({
|
||||
},
|
||||
{
|
||||
// The design systems this install RECORDS — for the projects it tracks,
|
||||
// not for the install itself. There was a sibling `/design` that read the
|
||||
// running app's own stylesheet out of the browser; it could only ever
|
||||
// inspect the instance it was served from, which made it a mirror rather
|
||||
// than a tool (#274).
|
||||
// not for the install itself. A page reading the running app's own
|
||||
// stylesheet out of the browser could only inspect the instance it was
|
||||
// served from, a mirror rather than a tool.
|
||||
path: "/design-systems",
|
||||
name: "design-systems",
|
||||
component: () => import("@/views/DesignSystemsView.vue"),
|
||||
|
||||
@@ -54,9 +54,8 @@ export const useTasksStore = defineStore("tasks", () => {
|
||||
|
||||
async function updateTask(
|
||||
id: number,
|
||||
// IssueFields carries `kind`, which the PATCH route now reads. It has
|
||||
// always been SENT by the task editor; until #3129 the route dropped it
|
||||
// and the save reported success while changing nothing.
|
||||
// IssueFields carries `kind`, which the PATCH route reads. A route that
|
||||
// dropped it would report success while changing nothing.
|
||||
data: Partial<
|
||||
Pick<Task, "title" | "body" | "tags" | "status" | "priority" | "due_date" | "project_id" | "milestone_id" | "parent_id" | "recurrence_rule">
|
||||
> & IssueFields
|
||||
|
||||
@@ -7,11 +7,10 @@
|
||||
* position more often has a trigger that fires on the wrong work. Telling an
|
||||
* operator to delete one of those would be the wrong nudge about half the time.
|
||||
*
|
||||
* The table lives here because the copy was about to exist in five places.
|
||||
* Three already had their own: `SnippetListView`, `RuleListPane`, and
|
||||
* `LessonDetailView` with the sentence inline in its template. The unified
|
||||
* Knowledge browse renders notes, tasks, processes, snippets and lessons in one
|
||||
* mixed feed (#4230), so it needs all of them at once — and a per-view constant
|
||||
* The table lives here because several surfaces need it: `SnippetListView`,
|
||||
* `RuleListPane`, `LessonDetailView`, and the unified Knowledge browse, which
|
||||
* renders notes, tasks, processes, snippets and lessons in one mixed feed and
|
||||
* so needs all of them at once — and a per-view constant
|
||||
* is how three surfaces end up giving three different answers to the same
|
||||
* question.
|
||||
*
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
/**
|
||||
* Turning a design system's RECORDED values into ones you can look at.
|
||||
*
|
||||
* Replaces `designTokens.ts` and `designDrift.ts`, which between them read the
|
||||
* running app's own stylesheet — names out of a bundled `theme.css`, values out
|
||||
* of `getComputedStyle(document.documentElement)`. That could only ever describe
|
||||
* the install serving the page, and the design surface is for the projects an
|
||||
* install TRACKS (#274). What is left here works on any system's record,
|
||||
* including one for an app this browser has never loaded.
|
||||
* Reading the running app's own stylesheet — names out of a bundled
|
||||
* `theme.css`, values out of `getComputedStyle(document.documentElement)` —
|
||||
* could only describe the install serving the page, and the design surface is
|
||||
* for the projects an install TRACKS. Everything here works on any system's
|
||||
* record, including one for an app this browser has never loaded.
|
||||
*
|
||||
* Nothing in this module reads the document's own tokens or mutates the page.
|
||||
*/
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* Design systems — the stylesheets this install RECORDS, for the projects it
|
||||
* tracks (milestone #254 step 5).
|
||||
* tracks.
|
||||
*
|
||||
* It had a sibling, `/design`, which showed the system as the BROWSER had it —
|
||||
* names out of a bundled stylesheet, values out of `getComputedStyle`. That
|
||||
* could only ever describe the install serving the page, so it was a mirror
|
||||
* rather than a tool and was retired (#274). Everything here works on a system
|
||||
* whose app this browser has never loaded.
|
||||
* Showing the system as the BROWSER has it — names out of a bundled
|
||||
* stylesheet, values out of `getComputedStyle` — could only describe the
|
||||
* install serving the page, a mirror rather than a tool. Everything here works
|
||||
* on a system whose app this browser has never loaded.
|
||||
*
|
||||
* The layout follows the model rather than decorating it. A system with a
|
||||
* parent holds only what it changes, so this page has two lists and they are
|
||||
@@ -153,7 +152,7 @@ const newTitle = ref("");
|
||||
const newDescription = ref("");
|
||||
const newParentId = ref<number | null>(null);
|
||||
const creating = ref(false);
|
||||
// Starter roles (#2349). The picker fills these on mount; empty means the
|
||||
// Starter roles. The picker fills these on mount; empty means the
|
||||
// operator unchecked everything, which is a real answer.
|
||||
const starterGroups = ref<string[]>([]);
|
||||
const tokenPrefix = ref("");
|
||||
@@ -500,8 +499,8 @@ watch(selectedId, () => {
|
||||
* `var()` reference states nothing on its own: rendering
|
||||
* `color-mix(in srgb, var(--accent) 15%, transparent)` as a background resolves
|
||||
* `--accent` against THIS app, so a system that isn't the one Scribe runs on
|
||||
* would be drawn in Scribe's palette. It looked right, which is why it went
|
||||
* unnoticed (#274).
|
||||
* would be drawn in Scribe's palette — and look right, which is why it would
|
||||
* go unnoticed.
|
||||
*
|
||||
* The preview above resolves values properly, on a probe carrying only that
|
||||
* system's declarations. So this list draws only what needs no resolving, and
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
*
|
||||
* IT BINDS NOBODY, and the page says so once, plainly. That sentence is the
|
||||
* difference between this kind and a rule, and it is the reason the kind
|
||||
* exists (#3727) — sessions were proposing rules for things that should never
|
||||
* have bound anyone.
|
||||
* exists — without it, sessions propose rules for things that should never
|
||||
* bind anyone.
|
||||
*
|
||||
* BUT IT CAN POINT AT ONE. A lesson that is an instance of a
|
||||
* rule says so, and the page shows which of the three answers it holds: the
|
||||
|
||||
@@ -231,7 +231,7 @@ async function save(force = false) {
|
||||
? await updateLesson(lessonId.value, payload)
|
||||
: await createLesson(payload);
|
||||
toast.show(isEdit.value ? "Lesson updated" : "Lesson recorded");
|
||||
// "No rule fits" just completed a group of lessons in one situation (#4634).
|
||||
// "No rule fits" just completed a group of lessons in one situation.
|
||||
// Said once, here, because the page this goes to does not recompute it.
|
||||
if (saved.convergence) {
|
||||
toast.show(
|
||||
|
||||
@@ -116,8 +116,9 @@ function truncate(text: string | null, max = 120): string {
|
||||
return text.length > max ? text.slice(0, max) + "..." : text;
|
||||
}
|
||||
|
||||
// A card is a glance, not a report. Roundtable had ~35 milestones and its tile
|
||||
// ran several viewport-heights tall, which made the grid unreadable (#2391).
|
||||
// A card is a glance, not a report. A project with dozens of milestones would
|
||||
// otherwise run its tile several viewport-heights tall and make the grid
|
||||
// unreadable.
|
||||
const MAX_MILESTONE_BARS = 10;
|
||||
|
||||
interface MilestoneBar extends MilestoneSummary {
|
||||
|
||||
@@ -977,8 +977,6 @@ onMounted(async () => {
|
||||
// Load user profile
|
||||
await loadProfile();
|
||||
|
||||
// Load journal config (locations, temp unit; prep/closeout UI removed in Phase 7)
|
||||
|
||||
// Check SearXNG status
|
||||
try {
|
||||
const sr = await apiGet<{ configured: boolean; searxng_url: string }>("/api/settings/search");
|
||||
|
||||
@@ -35,7 +35,7 @@ function locParts(loc: { repo: string; path: string; symbol: string }): string[]
|
||||
return [loc.repo, loc.path, loc.symbol].filter((p) => p && p.trim());
|
||||
}
|
||||
|
||||
// Un-merge (#2165)
|
||||
// Un-merge
|
||||
const unmerging = ref<number | null>(null);
|
||||
|
||||
/** Only entries carrying what the source contributed can be reversed exactly.
|
||||
|
||||
@@ -119,9 +119,6 @@ async function toggleSubTask(sub: SubTask) {
|
||||
const showPreview = ref(false);
|
||||
const sidebarOpen = ref(true);
|
||||
|
||||
// reconsolidate / isBodyAutoMaintained removed in Phase 8 — the curator
|
||||
// that auto-maintained task bodies is gone, so the body editor is now
|
||||
// always user-controlled.
|
||||
const editorRef = ref<InstanceType<typeof TiptapEditor> | null>(null);
|
||||
const titleRef = ref<HTMLInputElement | null>(null);
|
||||
const tiptapEditor = computed<Editor | null>(() => {
|
||||
|
||||
@@ -8,13 +8,13 @@ like. Three ways it goes wrong, all of them silent:
|
||||
|
||||
1. A rule is deleted and its `:hover` / modifier survives. The selector still
|
||||
exists, so nothing reads as unused, but the element renders with no base
|
||||
styling at all. `.btn-workspace:hover` outlived `.btn-workspace` and a
|
||||
router-link rendered as raw browser blue for days.
|
||||
styling at all — `.btn:hover` outliving `.btn` leaves a link in raw
|
||||
browser blue.
|
||||
|
||||
2. The parent's layout rule is deleted while the children keep theirs.
|
||||
`.milestone-header` was a flex row; its children still declare `flex: 1`
|
||||
and `flex-shrink: 0`. Without the parent they stack vertically, and a
|
||||
milestone that was one line becomes five. Nothing errors — the page just
|
||||
A header that is a flex row loses its rule; its children still declare
|
||||
`flex: 1` and `flex-shrink: 0`. Without the parent they stack vertically,
|
||||
and a row that was one line becomes five. Nothing errors — the page just
|
||||
wastes space, which reads as a design decision.
|
||||
|
||||
3. A rule is removed from a comma-separated group, leaving `.a,` dangling in
|
||||
@@ -35,9 +35,8 @@ Descendant selectors count as a base — `.panel .row {}` styles `.row` — beca
|
||||
from the element's side there is no difference. Only the LAST compound of a
|
||||
selector is what it styles, and a compound's whole class SET is what it
|
||||
requires: `.pane.empty` and `td.num` are base rules for the element carrying
|
||||
those classes, not modifiers. Reading them as modifiers cost this check four
|
||||
false reports on its first run, and a check with false reports is one that gets
|
||||
skimmed.
|
||||
those classes, not modifiers. Reading them as modifiers produces false
|
||||
reports, and a check with false reports is one that gets skimmed.
|
||||
|
||||
REPORT, NOT FAIL. Bare wrappers with no styling of their own are legitimate,
|
||||
so this cannot be a gate without a suppression mechanism nobody would maintain.
|
||||
@@ -46,11 +45,10 @@ say so in a comment beside its descendant rules — the remaining reports here
|
||||
all carry one, so a new entry means something changed.
|
||||
|
||||
KNOWN BLIND SPOT: a class with NO rule anywhere is invisible to this, because
|
||||
it cannot be told from a semantic-only hook. `.systems-list` had lost its
|
||||
entire rule and was rendering with browser bullets; it was found by reading the
|
||||
file next to a class that WAS half-styled, not by this check.
|
||||
it cannot be told from a semantic-only hook. A list whose entire rule is gone
|
||||
renders with browser bullets, and only reading the file finds it.
|
||||
|
||||
INSTANCE-AGNOSTIC (rule #115). Nothing here knows a class name, a component, or
|
||||
INSTANCE-AGNOSTIC. Nothing here knows a class name, a component, or
|
||||
a convention; point it at any Vue tree.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Check the frontend's CSS against the tokens its stylesheet declares.
|
||||
|
||||
The gap this closes: `services/design_stylesheet.check_code_against_tokens` has
|
||||
always been able to answer "does this code use the sheet correctly?", but the
|
||||
only thing ever fed to it was recorded SNIPPETS. The app's own components — where
|
||||
sixteen unresolvable references were found living quietly (#2319) — were checked
|
||||
by nothing at all.
|
||||
`services/design_stylesheet.check_code_against_tokens` answers "does this code
|
||||
use the sheet correctly?", but inside the app it is fed only recorded SNIPPETS.
|
||||
The app's own components, where unresolvable references live quietly, need a
|
||||
check of their own.
|
||||
|
||||
That was structural rather than an oversight. The drift panel runs in the browser
|
||||
and cannot read source files, and the server has no repo access. CI is the only
|
||||
place holding both the component sources and the ability to run the check, and it
|
||||
only became cheap once `theme.css` became a generated artifact — so the source of
|
||||
truth is a local file, with no network and no credentials.
|
||||
That is structural. The drift panel runs in the browser and cannot read source
|
||||
files, and the server has no repo access. CI is the only place holding both the
|
||||
component sources and the ability to run the check, and `theme.css` is a
|
||||
generated artifact — so the source of truth is a local file, with no network
|
||||
and no credentials.
|
||||
|
||||
INSTANCE-AGNOSTIC ON PURPOSE (rule #115). Nothing here knows what a token should
|
||||
INSTANCE-AGNOSTIC ON PURPOSE. Nothing here knows what a token should
|
||||
be called or which literals are discouraged. Both come from the stylesheet: the
|
||||
declarations, and the `SUPERSEDES` block the generator emits. Point it at a
|
||||
different install's sheet and it checks that install's rules.
|
||||
|
||||
+89
-113
@@ -4,47 +4,35 @@
|
||||
WHY THIS EXISTS. `plugin/` is not built into the Docker image. Installs fetch it
|
||||
from this git repo via `.claude-plugin/marketplace.json`, so a push IS the
|
||||
release for plugin content: no build, no gate, immediately fetchable. Two
|
||||
separate defects have reached a live install through that path:
|
||||
failures reach an install through that path without a sound:
|
||||
|
||||
- #2198 — all four hook scripts were inert (wrong env-var case, line-oriented
|
||||
`jq -rR`, line-oriented `cut -c`). No CI ran, because `plugin/**` wasn't in
|
||||
the workflow's `paths:` filter at all.
|
||||
- #2209 — the fix for #2198 shipped to `main` and still couldn't reach an
|
||||
install, because `plugin.json`'s version wasn't bumped and the installer
|
||||
compares versions to decide whether to refresh its cache.
|
||||
- inert hook scripts (a wrong env-var case, a line-oriented `cut -c`) — so
|
||||
`plugin/**` is in the workflow's `paths:` filter and these checks run on it;
|
||||
- a fix that ships and cannot reach an install, because `plugin.json`'s
|
||||
version did not move and the installer compares versions to decide whether
|
||||
to refresh its cache.
|
||||
|
||||
Both were fixed. The second was fixed TWICE — once by bumping the number, and
|
||||
then properly, by removing the class it came from: `plugin.json`'s version is
|
||||
no longer a value anybody chooses. `scripts/mint_plugin_version.py` derives it
|
||||
from the clock (`make mint-plugin`), and `check_version_is_minted` below fails
|
||||
the lane when shipped content moved and the version did not.
|
||||
`plugin.json`'s version is not a value anybody chooses.
|
||||
`scripts/mint_plugin_version.py` derives it from the clock (`make mint-plugin`),
|
||||
and `check_version_is_minted` below fails the lane when shipped content moved
|
||||
and the version did not.
|
||||
|
||||
State exactly what that did and did not remove, because a rationale that
|
||||
overstates its own control is how the control gets trusted past its limit, and
|
||||
because the paragraph this replaces was itself read that way. Gone: having to
|
||||
remember which NUMBER to write, and the whole question of whether a chosen
|
||||
number was the right one. Not gone: the mint still has to be RUN, and
|
||||
forgetting to run it is still possible. What changed is that forgetting is now
|
||||
LOUD — a red lane on the batch that forgot, instead of a silent no-op found
|
||||
weeks later when somebody says "I don't think it updated" (#2220).
|
||||
What that does and does not remove, stated exactly, because a rationale that
|
||||
overstates its own control is how the control gets trusted past its limit.
|
||||
Gone: choosing which NUMBER to write, and the question of whether a chosen
|
||||
number was the right one. Not gone: the mint still has to be RUN. What the
|
||||
check adds is that forgetting is LOUD — a red lane on the batch that forgot,
|
||||
instead of a silent no-op found weeks later when somebody says "I don't think
|
||||
it updated".
|
||||
|
||||
shellcheck is NOT in `ci-python` (verified against CI-runner's Dockerfile and
|
||||
scripts/install-common.sh, not from memory — rule #37). CI installs it per-job,
|
||||
which is what CI-runner's own docs/process.md prescribes for a dep with a
|
||||
single consumer: "If only one project needs the dep, prefer that project
|
||||
installing it per-job in their workflow — at least until a second consumer
|
||||
arrives." Promotion into the image is filed as an issue there rather than
|
||||
assumed here.
|
||||
shellcheck is NOT in the `ci-python` image; CI installs it per-job, the usual
|
||||
practice for a dependency with a single consumer. It is optional at runtime:
|
||||
without shellcheck the lint step is SKIPPED and says so. A skipped check
|
||||
announces itself loudly, because a check that quietly no-ops is the failure
|
||||
mode this whole file exists to prevent.
|
||||
|
||||
It is optional at runtime: without shellcheck the lint step is SKIPPED and says
|
||||
so. A skipped check announces itself loudly, because a check that quietly
|
||||
no-ops is the failure mode this whole file exists to prevent.
|
||||
|
||||
jq USED TO BE IN THAT SAME SENTENCE, and the smoke tests below skipped
|
||||
themselves without it. Since jq is not in `ci-python` either, that meant three
|
||||
of them announced a skip on every CI run and had never actually run there. The
|
||||
hooks need no jq now (#4107), so those checks run unconditionally — removing a
|
||||
dependency from the product removed a permanent hole in its verification.
|
||||
The hooks need no jq (it is not in `ci-python` either), so the smoke tests
|
||||
below run unconditionally rather than skipping wherever it is absent.
|
||||
|
||||
Usage:
|
||||
python3 scripts/check_plugin.py # all checks
|
||||
@@ -85,7 +73,7 @@ MANIFEST = PLUGIN_DIR / ".claude-plugin" / "plugin.json"
|
||||
|
||||
# ── What ships, and what decides what it says about itself ─────────────────
|
||||
#
|
||||
# ONE definition (#3127 §3, milestone 334 step 2). It has TWO consumers that
|
||||
# ONE definition. It has TWO consumers that
|
||||
# need different granularities, and conflating them is the bug:
|
||||
#
|
||||
# the workflow's `paths:` trigger whole paths should CI run at all?
|
||||
@@ -103,15 +91,14 @@ MANIFEST = PLUGIN_DIR / ".claude-plugin" / "plugin.json"
|
||||
# carries description, mcpServers and userConfig, all of which reach an
|
||||
# install and all of which matter. Excluding the file wholesale would mean a
|
||||
# userConfig-only edit computes an unchanged version and never refreshes —
|
||||
# #2209 again with a narrower trigger.
|
||||
# the unreached fix again, with a narrower trigger.
|
||||
SHIPPED_PATHS = ("plugin", ".claude-plugin")
|
||||
|
||||
# Files that decide what a published artifact SAYS ABOUT ITSELF — kept as a
|
||||
# table so the next artifact is a one-line addition rather than a third
|
||||
# bespoke guard (#3127 §3). The membership test is NOT "is this copied into
|
||||
# the artifact?" but "can changing this file change the published bytes, or
|
||||
# what the artifact says about itself?" — FC learned that twice in four days
|
||||
# (#3156, #3202), and a deriver is never in the COPY list.
|
||||
# bespoke guard. The membership test is NOT "is this copied into the
|
||||
# artifact?" but "can changing this file change the published bytes, or what
|
||||
# the artifact says about itself?" — a deriver is never in the COPY list.
|
||||
#
|
||||
# Note what is absent: a CHECKER does not belong here. Whatever validates a
|
||||
# version decides whether the lane goes red, not what any artifact reports,
|
||||
@@ -119,10 +106,10 @@ SHIPPED_PATHS = ("plugin", ".claude-plugin")
|
||||
# the plugin version is.
|
||||
DERIVERS: dict[str, tuple[str, ...]] = {
|
||||
# The "Generate image tags and version" step computes the server image's
|
||||
# name, ordering key and channel (#3298).
|
||||
# name, ordering key and channel.
|
||||
".forgejo/workflows/ci.yml": ("server-image",),
|
||||
# Decides the plugin's version FORMAT, so it decides what every future
|
||||
# manifest says about itself (milestone 334 step 3).
|
||||
# manifest says about itself.
|
||||
"scripts/mint_plugin_version.py": ("plugin",),
|
||||
}
|
||||
|
||||
@@ -130,7 +117,7 @@ DERIVERS: dict[str, tuple[str, ...]] = {
|
||||
def version_relevant_paths() -> tuple[str, ...]:
|
||||
"""Everything a change to which must produce a NEW plugin version.
|
||||
|
||||
Wider than `SHIPPED_PATHS`, and #3127 §3's asymmetry is why it has to be:
|
||||
Wider than `SHIPPED_PATHS`, and this asymmetry is why it has to be:
|
||||
|
||||
A change to how the VERSION is computed is compared against nothing at
|
||||
all. Left out, the published artifact goes on reporting the OLD value
|
||||
@@ -140,7 +127,7 @@ def version_relevant_paths() -> tuple[str, ...]:
|
||||
and a diff over the shipped paths alone reports "no content change, the
|
||||
version need not move". The manifest then keeps a value in the old format
|
||||
forever and nothing ever says so. The mint script reaches no install and
|
||||
belongs here anyway; that is #3156's exact shape.
|
||||
belongs here anyway.
|
||||
|
||||
A CHECKER is deliberately not here. Whatever validates the version decides
|
||||
whether the lane goes red, not what any artifact reports — so this file is
|
||||
@@ -165,7 +152,7 @@ def ok(msg: str) -> None:
|
||||
|
||||
def skip(msg: str) -> None:
|
||||
# Loud on purpose. A check that quietly does nothing is indistinguishable
|
||||
# from a check that passed — the exact confusion that let #2198 survive.
|
||||
# from a check that passed.
|
||||
print(f"SKIP {msg}")
|
||||
|
||||
|
||||
@@ -191,7 +178,7 @@ def check_syntax() -> None:
|
||||
# --- known-bad patterns ----------------------------------------------------
|
||||
|
||||
# Each entry: (compiled pattern, short label, why it's wrong).
|
||||
# These are the exact classes from #2198. They are deliberately specific — a
|
||||
# These are the classes that make a hook inert. They are deliberately specific — a
|
||||
# broad shell linter belongs in the image, not hand-rolled here.
|
||||
PATTERNS: list[tuple[re.Pattern, str, str]] = [
|
||||
(
|
||||
@@ -202,15 +189,11 @@ PATTERNS: list[tuple[re.Pattern, str, str]] = [
|
||||
"hook then does nothing, silently.",
|
||||
),
|
||||
(
|
||||
# NEITHER OF THESE MAY COME BACK (#4107). This replaced a narrower rule
|
||||
# about `jq -R` being line-oriented, which is now moot in the only way
|
||||
# that rule could become moot: there is no jq left to pass flags to.
|
||||
#
|
||||
# The wider rule is the one worth having. jq is absent by default on
|
||||
# NEITHER OF THESE BELONGS IN A HOOK. jq is absent by default on
|
||||
# macOS, on the Debian/Ubuntu slim images, on Alpine and in most CI
|
||||
# containers; `tac` is GNU-only and absent on macOS. Every hook used to
|
||||
# guard itself with `command -v jq || exit 0`, so a machine without it
|
||||
# got no context, no rules, no prior art and no process sync, silently —
|
||||
# containers; `tac` is GNU-only and absent on macOS. A hook that guards
|
||||
# itself with `command -v jq || exit 0` gives a machine without it no
|
||||
# context, no rules, no prior art and no process sync, silently —
|
||||
# `exit 0` is indistinguishable from "nothing to say". A hook may use
|
||||
# only what POSIX guarantees; scribe_defs.sh has the readers.
|
||||
re.compile(r"(?<![\w./-])(jq|tac)(?![\w./-])"),
|
||||
@@ -219,15 +202,15 @@ PATTERNS: list[tuple[re.Pattern, str, str]] = [
|
||||
"by default anywhere the plugin is likely to land, and tac is GNU-only. "
|
||||
"Read JSON with scribe_json_flat / _pick / _list / _len, write it with "
|
||||
"scribe_json_out, encode with scribe_urlenc; to take the LAST match of "
|
||||
"a filter, pipe to `tail -1` instead of reversing with tac (#4107).",
|
||||
"a filter, pipe to `tail -1` instead of reversing with tac.",
|
||||
),
|
||||
(
|
||||
# `$(producer | head -N) || var=""` — the fallback OUTSIDE the
|
||||
# substitution (#4042). Every hook runs under `pipefail`; once `head`
|
||||
# substitution. Every hook runs under `pipefail`; once `head`
|
||||
# (or `grep -m`/`-q`) has what it needs it exits, a producer still
|
||||
# writing dies of SIGPIPE, the substitution fails, and the fallback
|
||||
# wipes the output that was already captured. Silent, and only on big
|
||||
# inputs — which for the duplicate arm meant the most-duplicated names.
|
||||
# inputs — which for the duplicate arm means the most-duplicated names.
|
||||
re.compile(r"\|\s*(?:head\b|grep\s+-[A-Za-z]*[mq])[^)]*\)\s*\|\|\s*\w+=\"\""),
|
||||
"early-exit consumer with the fallback outside the substitution",
|
||||
"Under pipefail, `$(x | head -N) || v=\"\"` discards head's output whenever x "
|
||||
@@ -298,7 +281,7 @@ def check_shellcheck() -> None:
|
||||
# always emits a static behavioural floor that needs no credentials and no
|
||||
# network, so "silent" would be the wrong assertion for it.
|
||||
#
|
||||
# UNREACHABLE is different for the two write-path hooks since #2932: a call
|
||||
# UNREACHABLE is different for the two write-path hooks: a call
|
||||
# that was owed and did not come back is SAID, once per outage ("> Scribe did
|
||||
# not answer …"), so a session can tell "checked, nothing there" from "never
|
||||
# checked". That line — or silence, when the once-per-outage marker in
|
||||
@@ -306,19 +289,17 @@ def check_shellcheck() -> None:
|
||||
# is the only output allowed with no working instance; anything else is a hook
|
||||
# speaking on data it cannot have.
|
||||
#
|
||||
# This is the contract that made #2198 invisible for weeks, so it is worth
|
||||
# pinning: the bug and the healthy no-results case looked identical from
|
||||
# outside. #2932 is what finally makes the failure visible at the write; this
|
||||
# check makes sure the fail-open behaviour stays deliberate rather than
|
||||
# accidental.
|
||||
# The contract is worth pinning because an inert hook and the healthy
|
||||
# no-results case look identical from outside. The outage line makes the
|
||||
# failure visible at the write; this check makes sure the fail-open behaviour
|
||||
# stays deliberate rather than accidental.
|
||||
# A symbol that exists nowhere, ASSEMBLED rather than written literally.
|
||||
# The prior-art hook's local arm (#2280) fires with no credentials, so the
|
||||
# silence assertion below needs a name the repo genuinely lacks. Two traps,
|
||||
# both hit while writing this:
|
||||
# - `def f` matched real code, so the hook spoke and "silent" was asserting
|
||||
# the wrong thing;
|
||||
# - spelling the replacement out in full put `def <name>(` INTO this file,
|
||||
# so the smoke event defined the very symbol it claimed was absent.
|
||||
# The prior-art hook's local arm fires with no credentials, so the silence
|
||||
# assertion below needs a name the repo genuinely lacks. Two traps:
|
||||
# - a short name like `def f` matches real code, so the hook speaks and
|
||||
# "silent" asserts the wrong thing;
|
||||
# - spelling the replacement out in full puts `def <name>(` INTO this file,
|
||||
# so the smoke event defines the very symbol it claims is absent.
|
||||
# Concatenating keeps the contiguous string out of the source.
|
||||
_ABSENT_SYM = "zz" + "_absent_" + "9f3a2b"
|
||||
|
||||
@@ -331,7 +312,7 @@ SMOKE_EVENTS: dict[str, str] = {
|
||||
"tool_input": {"file_path": "src/x.py",
|
||||
"new_string": f"def {_ABSENT_SYM}():\n pass\n"}}
|
||||
),
|
||||
# The pre-tool rule arm (#3476). A real Bash call, and one whose whole
|
||||
# The pre-tool rule arm. A real Bash call, and one whose whole
|
||||
# point is that it looks harmless: reaching for curl against the forge API
|
||||
# is the reflex the arm exists to catch. With no instance it must stay
|
||||
# SILENT — it is deliberately not an OUTAGE_SPEAKER, because a Bash call is
|
||||
@@ -341,7 +322,7 @@ SMOKE_EVENTS: dict[str, str] = {
|
||||
{"session_id": "smoke", "cwd": ".", "tool_name": "Bash",
|
||||
"tool_input": {"command": "curl -s https://example.invalid/api/v1/runs"}}
|
||||
),
|
||||
# The moment arm (milestone 458). A command the shipped mappings send to
|
||||
# The moment arm. A command the shipped mappings send to
|
||||
# work.deliver, so a configured run reaches the moment-tools fetch and the
|
||||
# moment request; with no instance both fail and it must stay SILENT,
|
||||
# since it fires before every call that can reach a mounted rule.
|
||||
@@ -351,7 +332,7 @@ SMOKE_EVENTS: dict[str, str] = {
|
||||
),
|
||||
"scribe_sync_processes.sh": json.dumps({"source": "startup"}),
|
||||
"scribe_session_context.sh": json.dumps({"source": "startup"}),
|
||||
# The after-write hook (#2901) diffs the working tree; on CI's clean
|
||||
# The after-write hook diffs the working tree; on CI's clean
|
||||
# checkout there is nothing to report, so silence is the right assertion.
|
||||
# (On a dirty local tree with a definition just written it may speak —
|
||||
# that is the hook working, not a failure of the contract.)
|
||||
@@ -359,27 +340,27 @@ SMOKE_EVENTS: dict[str, str] = {
|
||||
{"session_id": "smoke", "cwd": ".", "tool_name": "Bash",
|
||||
"tool_input": {"command": "true"}, "tool_response": {}}
|
||||
),
|
||||
# The Stop-hook report check (milestone 409 step 5). A transcript that does
|
||||
# The Stop-hook report check. A transcript that does
|
||||
# not exist is the smoke case: nothing to read, so it must stay silent and
|
||||
# never block, configured or not.
|
||||
"scribe_report_check.sh": json.dumps(
|
||||
{"session_id": "smoke", "transcript_path": "/nonexistent/smoke.jsonl",
|
||||
"cwd": ".", "hook_event_name": "Stop", "stop_hook_active": False}
|
||||
),
|
||||
# The reply moment (milestone 458). No transcript, so no reply: silent and
|
||||
# The reply moment. No transcript, so no reply: silent and
|
||||
# never a block, configured or not.
|
||||
"scribe_reply_check.sh": json.dumps(
|
||||
{"session_id": "smoke", "transcript_path": "/nonexistent/smoke.jsonl",
|
||||
"cwd": ".", "hook_event_name": "Stop", "stop_hook_active": False}
|
||||
),
|
||||
# The end-of-turn shape check (milestone 439). With no ledger for the
|
||||
# The end-of-turn shape check. With no ledger for the
|
||||
# session there is nothing to ask about, and with no instance there is
|
||||
# nobody to ask: silent, and never a block, configured or not.
|
||||
"scribe_shape_check.sh": json.dumps(
|
||||
{"session_id": "smoke", "transcript_path": "/nonexistent/smoke.jsonl",
|
||||
"cwd": ".", "hook_event_name": "Stop", "stop_hook_active": False}
|
||||
),
|
||||
# The PreCompact preserver (#3680). Its whole contract is the inverse of
|
||||
# The PreCompact preserver. Its whole contract is the inverse of
|
||||
# every other hook's: it must exit 0 AND print, because stdout is what
|
||||
# becomes the summarizer's custom instructions. A silent success here is
|
||||
# the failure mode, and it would look like every other hook's success.
|
||||
@@ -387,7 +368,7 @@ SMOKE_EVENTS: dict[str, str] = {
|
||||
{"session_id": "smoke", "cwd": ".", "hook_event_name": "PreCompact",
|
||||
"trigger": "manual", "custom_instructions": None}
|
||||
),
|
||||
# The opened-ledger recorder (#4100). It writes to TMPDIR and prints
|
||||
# The opened-ledger recorder. It writes to TMPDIR and prints
|
||||
# nothing — a PostToolUse hook that emitted output would put a line in
|
||||
# front of every get_rule call, which is the opposite of its purpose. The
|
||||
# smoke case is a well-formed open: it must exit 0 and stay silent.
|
||||
@@ -396,18 +377,17 @@ SMOKE_EVENTS: dict[str, str] = {
|
||||
"tool_name": "mcp__scribe__get_rule", "tool_input": {"rule_id": 1},
|
||||
"tool_response": {}}
|
||||
),
|
||||
# The acted-ledger recorder (#4216). Same shape and same reasoning as its
|
||||
# sibling above: TMPDIR only, and silent, because a PostToolUse hook that
|
||||
# spoke would put a line after every rule_outcome call and turn recording
|
||||
# an outcome into something with a cost. A skip here would have left the
|
||||
# newest of the three ledgers as the only hook the lane never runs.
|
||||
# The acted-ledger recorder. Same shape and same reasoning as its sibling
|
||||
# above: TMPDIR only, and silent, because a PostToolUse hook that spoke
|
||||
# would put a line after every rule_outcome call and turn recording an
|
||||
# outcome into something with a cost.
|
||||
"scribe_record_outcome.sh": json.dumps(
|
||||
{"session_id": "smoke", "cwd": ".",
|
||||
"tool_name": "mcp__scribe__rule_outcome",
|
||||
"tool_input": {"rule_id": 1, "outcome": "applied"},
|
||||
"tool_response": {}}
|
||||
),
|
||||
# The claim-session binder (milestone 381). Silent like the ledgers above —
|
||||
# The claim-session binder. Silent like the ledgers above —
|
||||
# a PostToolUse hook that spoke would add a line after every task write —
|
||||
# and with no instance configured it must exit before reaching for one.
|
||||
"scribe_claim_session.sh": json.dumps(
|
||||
@@ -416,7 +396,7 @@ SMOKE_EVENTS: dict[str, str] = {
|
||||
"tool_input": {"task_id": 1, "content": "smoke"},
|
||||
"tool_response": {}}
|
||||
),
|
||||
# The SessionEnd claim release (milestone 381 step 4). Silent: nobody reads
|
||||
# The SessionEnd claim release. Silent: nobody reads
|
||||
# a SessionEnd hook's output, and with no instance it must exit first.
|
||||
"scribe_session_end.sh": json.dumps(
|
||||
{"session_id": "smoke", "cwd": ".", "hook_event_name": "SessionEnd",
|
||||
@@ -435,14 +415,14 @@ STATIC_EMITTERS = {
|
||||
# The two-tier SessionStart design: the bundled static tier ships whatever
|
||||
# the instance does, and that floor is the whole point of the split.
|
||||
"scribe_session_context.sh": "static floor present",
|
||||
# PreCompact's contract is INVERTED (#3680). Its stdout becomes the
|
||||
# PreCompact's contract is INVERTED. Its stdout becomes the
|
||||
# summarizer's custom instructions, and what must survive a summary is
|
||||
# known without asking anything — so it needs no instance, and silence is
|
||||
# the failure mode rather than the success one. Read as a generic hook it
|
||||
# would look like a leak; it is the opposite.
|
||||
"scribe_precompact_preserve.sh": "preservation instructions present",
|
||||
}
|
||||
# The hooks that say so when a configured instance does not answer (#2932).
|
||||
# The hooks that say so when a configured instance does not answer.
|
||||
OUTAGE_SPEAKERS = {"scribe_prior_art.sh", "scribe_after_write.sh"}
|
||||
OUTAGE_LINE = "> Scribe did not answer the prior-art check"
|
||||
|
||||
@@ -458,13 +438,9 @@ def _run_hook(script: Path, event: str, env_extra: dict[str, str]) -> subprocess
|
||||
|
||||
|
||||
def check_fail_open() -> None:
|
||||
# No jq gate any more (#4107). This check used to skip itself when jq was
|
||||
# missing, because without it every hook bailed at line 1 and the check
|
||||
# would have passed while exercising nothing. jq is NOT in `ci-python`, so
|
||||
# what that actually meant is that this smoke test announced a skip on
|
||||
# every CI run and never once ran there. The hooks now need only POSIX
|
||||
# tools, so it runs everywhere — which is the point of the change it is
|
||||
# testing.
|
||||
# No jq gate: the hooks need only POSIX tools, so this runs everywhere,
|
||||
# CI's `ci-python` image included. A gate on a tool the image lacks would
|
||||
# announce a skip on every run and never exercise the hooks there.
|
||||
scenarios = [
|
||||
("unconfigured", {}),
|
||||
# Connection refused immediately — exercises the unreachable-instance
|
||||
@@ -514,7 +490,7 @@ def check_fail_open() -> None:
|
||||
|
||||
|
||||
def check_local_prior_art_needs_no_instance() -> None:
|
||||
"""The prior-art hook's local arm must answer with no credentials (#2280).
|
||||
"""The prior-art hook's local arm must answer with no credentials.
|
||||
|
||||
The other arms ask Scribe what was RECORDED. This one asks the repo what
|
||||
EXISTS, which needs no instance — and that is the whole reason it catches
|
||||
@@ -562,8 +538,8 @@ def check_session_context_reports_its_version() -> None:
|
||||
pulls on its own, while the CACHE is what executes and refreshes only when
|
||||
the manifest version changes. So a shipped fix can sit unreached while
|
||||
inspecting the clone shows it present — the obvious debugging move
|
||||
misleads, and twice the only detector was a human saying "I don't think it
|
||||
updated" (#2209, #2220).
|
||||
misleads, and otherwise the only detector is a human saying "I don't think
|
||||
it updated".
|
||||
|
||||
Asserted WITHOUT credentials on purpose. The state most needing diagnosis
|
||||
is the one where the token never arrives, and a marker that vanished there
|
||||
@@ -643,8 +619,8 @@ def manifest_differs_beyond_version(a: str | None, b: str | None) -> bool:
|
||||
or mcpServers-only edit demanding a new version.
|
||||
|
||||
Unreadable input answers True. The conservative direction is "demand a new
|
||||
version": a spurious bump costs one cache refresh, while a missed one is
|
||||
#2209 — the fix reaches the repo and stops there.
|
||||
version": a spurious bump costs one cache refresh, while a missed one means
|
||||
the fix reaches the repo and stops there.
|
||||
"""
|
||||
def without_version(text: str | None):
|
||||
if text is None:
|
||||
@@ -667,8 +643,8 @@ def shipped_content_changed(base: str) -> tuple[bool | None, list[str]]:
|
||||
"""Has anything that REACHES AN INSTALL changed against `base`?
|
||||
|
||||
Returns `(changed, paths)`. `changed` is **None** when the question could
|
||||
not be answered — a caller must never read that as "no", which is the
|
||||
distinction #2663 cost weeks of zeroed telemetry to learn.
|
||||
not be answered — a caller must never read that as "no": an unanswered
|
||||
question read as "nothing changed" is a failure that reports success.
|
||||
|
||||
The manifest is special-cased, not excluded: if it is the ONLY thing that
|
||||
moved and the only difference is `version`, nothing that reaches an
|
||||
@@ -693,17 +669,17 @@ def shipped_content_changed(base: str) -> tuple[bool | None, list[str]]:
|
||||
|
||||
|
||||
def check_version_is_minted(base: str = "origin/main") -> None:
|
||||
"""THE control (#3127 checklist 4), replacing "somebody remembers".
|
||||
"""THE control, replacing "somebody remembers".
|
||||
|
||||
The checklist asks, of any hand-set component: *say what happens the
|
||||
release somebody forgets it.* This is the answer — the lane goes red,
|
||||
deterministically, because CI can compute whether the value should have
|
||||
moved. Its predecessor could only ask "did the number move at all", which
|
||||
any bump satisfied and which therefore proved nothing.
|
||||
moved. Asking only "did the number move at all" would be satisfied by any
|
||||
bump and would therefore prove nothing.
|
||||
|
||||
Four verdicts:
|
||||
|
||||
content changed, version did not FAIL — this is #2209, exactly
|
||||
content changed, version did not FAIL — the fix would not reach an install
|
||||
version not in canonical shape FAIL — see below
|
||||
version implausibly in the future FAIL — a bad clock or a hand-edit
|
||||
version moved, content did not pass, and say so
|
||||
@@ -718,10 +694,10 @@ def check_version_is_minted(base: str = "origin/main") -> None:
|
||||
A malformed version is worth failing on even though the installer would
|
||||
accept it. `K4` returns the manifest string verbatim, and `H == "unknown"`
|
||||
sets `forceOverwrite`, so a broken value either sorts as a normal string
|
||||
or reinstalls the plugin every single session (#3325). Neither is loud.
|
||||
or reinstalls the plugin every single session. Neither is loud.
|
||||
|
||||
Stated against the BASE BRANCH rather than the last commit, as its
|
||||
predecessor was: a per-commit rule would demand a fresh mint from every
|
||||
Stated against the BASE BRANCH rather than the last commit: a per-commit
|
||||
rule would demand a fresh mint from every
|
||||
commit in a batch, when what matters is that whatever reaches an install
|
||||
differs from what is cached. One mint per batch, which is also how a person
|
||||
would do it.
|
||||
@@ -745,8 +721,8 @@ def check_version_is_minted(base: str = "origin/main") -> None:
|
||||
if not VERSION_RE.match(here):
|
||||
fail(
|
||||
f"the manifest version is {here!r}, which is not YYYY.MM.DD.HHMM.\n"
|
||||
f" One shape for every version in the family (#3127 checklist "
|
||||
f"10), zero-padded so the midnight case renders 2026.01.05.0000.\n"
|
||||
f" One shape for every version in the family, zero-padded so "
|
||||
f"the midnight case renders 2026.01.05.0000.\n"
|
||||
f" Run `make mint-plugin`."
|
||||
)
|
||||
return
|
||||
@@ -780,7 +756,7 @@ def check_version_is_minted(base: str = "origin/main") -> None:
|
||||
f" The installer decides whether to refresh its cache by "
|
||||
f"comparing this string, so an unchanged version means these edits "
|
||||
f"reach the repo and stop there — the marketplace clone updates, the "
|
||||
f"cache that actually executes does not (#2209, #1040, #2220).\n"
|
||||
f"cache that actually executes does not.\n"
|
||||
f" Run `make mint-plugin`.\n"
|
||||
f" Changed:\n {files}"
|
||||
)
|
||||
|
||||
@@ -7,8 +7,7 @@ A DRY audit made of several passes ends with a question no single pass can
|
||||
answer: did the audit as a whole reduce duplication, and did it create any? The
|
||||
second half is the one that matters. A pass that shortens two statements can
|
||||
leave them byte-identical to a third, and the new copy is invisible to that
|
||||
pass's own scan because it did not exist when the scan ran (#4745: a retro over
|
||||
a 17-pass audit found exactly this, and folded it into a view). This script is
|
||||
pass's own scan because it did not exist when the scan ran. This script is
|
||||
the close-out measure the DRY Pass process names, so the measure is a tool and
|
||||
not something each audit improvises.
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ Installs fetch it straight from this git repo via `.claude-plugin/
|
||||
marketplace.json`, so **a push IS the release** — there is no build between
|
||||
you committing and a user fetching, and therefore no moment at which CI could
|
||||
stamp a version in. Every other artifact in the family derives its version
|
||||
during a build (note #3127 §2). This one has no build to derive during.
|
||||
during a build. This one has no build to derive during.
|
||||
|
||||
WHICH CLOCK, AND WHY IT DIFFERS FROM THE SERVER IMAGE — the divergence is
|
||||
deliberate, and it lives one directory away from its opposite, so it is
|
||||
@@ -27,8 +27,7 @@ one lane and no build, so that reason does not reach it and paying its cost
|
||||
buys nothing. What is given up is reproducibility-from-history: you cannot
|
||||
recompute this value later, only verify that it moved when it had to.
|
||||
|
||||
That trade is acceptable ONLY because of what #3325 established by reading the
|
||||
installer's code: the refresh test is `P.version === H`, plain string
|
||||
That trade is acceptable ONLY because of what the installer's code does: the refresh test is `P.version === H`, plain string
|
||||
equality, with no ordering comparison anywhere. Where a comparator ORDERS, an
|
||||
unreproducible version is dangerous — nothing can check it is right. Where it
|
||||
only tests equality, "did it change when it should have" is the entire
|
||||
@@ -52,8 +51,8 @@ ROOT = Path(__file__).resolve().parents[1]
|
||||
MANIFEST = ROOT / "plugin" / ".claude-plugin" / "plugin.json"
|
||||
|
||||
# Four dot-separated numeric fields, zero-padded, and nothing else — one shape
|
||||
# for every human-readable version in the family (#3127 checklist 10). The
|
||||
# padding is load-bearing for the midnight case the checklist names by hand:
|
||||
# for every human-readable version in the family. The padding is
|
||||
# load-bearing for the midnight case:
|
||||
# 2026.01.05.0000, which an unpadded `%-H%M` would render as `0` and silently
|
||||
# shorten. Harmless while nothing orders these, wrong the moment anything does.
|
||||
VERSION_RE = re.compile(r"^\d{4}\.\d{2}\.\d{2}\.\d{4}$")
|
||||
|
||||
@@ -1,18 +1,9 @@
|
||||
"""Project CRUD MCP tools — thin wrappers over services/projects.py.
|
||||
|
||||
Mirrors existing fable-mcp project tool contracts. Note: there is no
|
||||
fable_delete_project here (matches existing fable-mcp surface). To stop
|
||||
working on a project, update its status to 'archived'.
|
||||
|
||||
The LLM-era similarity-check / 'confirmed' guard from services/tools/projects.py
|
||||
is intentionally NOT replicated here — Claude is the client, not a weak local
|
||||
model that needs that guardrail. services.projects.create_project creates
|
||||
directly with no similarity warning.
|
||||
|
||||
The auto-summary regeneration that services.projects.update_project triggers
|
||||
async will be removed in Phase 7 (it's an LLM call). The wrapper makes no
|
||||
assumption either way; once the service-layer side effect is gone, this code
|
||||
keeps working.
|
||||
There is no delete tool: to stop working on a project, set its status to
|
||||
'archived'. create_project creates directly, with no similarity check or
|
||||
'confirmed' guard — the client is a capable model that can read the project
|
||||
list itself.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
"""User settings + integrations (SearXNG status).
|
||||
|
||||
Chat-model picker endpoints (/models), KV-cache priming, and journal-schedule
|
||||
hooks were removed in Phase 8 alongside the chat/journal subsystems.
|
||||
"""
|
||||
"""User settings + integrations (SearXNG status)."""
|
||||
import logging
|
||||
|
||||
from quart import Blueprint, jsonify, request
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
"""Session-context rendering for the Scribe plugin's SessionStart hook.
|
||||
|
||||
The plugin's hook curls `GET /api/plugin/context` at session start and injects
|
||||
the returned text as `additionalContext`, giving Scribe the same push channel
|
||||
that superpowers and file-memory have. This module renders that text.
|
||||
the returned text as `additionalContext`, giving Scribe a push channel into
|
||||
the session. This module renders that text.
|
||||
|
||||
Design note — altitude: we inject rule *titles* grouped by topic (a compact
|
||||
index), NOT every rule's full statement. The 48 always-on statements run well
|
||||
past the 10k-char `additionalContext` cap, and the push channel's job is to make
|
||||
index), NOT every rule's full statement. A working rulebook's always-on statements run
|
||||
well past the 10k-char `additionalContext` cap, and the push channel's job is to make
|
||||
Claude *aware* the rules exist and *reach* for them — not to dump them. Full
|
||||
text stays one `get_rule(id)` / `search(content_type="rule")` call away. Titles are
|
||||
mostly self-describing ("`dev` is home", "No GitHub — Fabled-Git only"), so the
|
||||
index alone already steers behavior.
|
||||
mostly self-describing ("Run migrations before deploying"), so the index alone
|
||||
already steers behavior.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
@@ -1004,9 +1004,8 @@ async def restore_rule_version(
|
||||
) -> Optional[Rule]:
|
||||
"""Put a preference back to what it said. None when it is not readable.
|
||||
|
||||
MILESTONE 323 DECIDED THE OPPOSITE FOR RULES, and that decision stands —
|
||||
`routes/rulebooks.py` still has no restore for them. Its reasoning:
|
||||
"a binding instruction should not be revertible in one click", because a
|
||||
RULES DELIBERATELY HAVE NO RESTORE — `routes/rulebooks.py` offers none —
|
||||
because a binding instruction should not be revertible in one click: a
|
||||
silent revert erases the only record of why the rewrite happened.
|
||||
|
||||
A preference inverts both halves of that. The rewrite was not the
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
"""User profile service — structured per-user preferences.
|
||||
|
||||
Post-pivot the LLM-driven observation/consolidation surface is gone (curator
|
||||
deleted in Phase 8). The profile model is preserved as user-managed metadata
|
||||
(name, job, expertise, style, tone, interests, work schedule) — Claude can
|
||||
read it via the upcoming MCP profile tools.
|
||||
The profile is user-managed metadata (name, job, expertise, style, tone,
|
||||
interests, work schedule), read and written through /api/profile and carried
|
||||
in backups.
|
||||
"""
|
||||
import logging
|
||||
|
||||
|
||||
Reference in New Issue
Block a user