Compare commits
88
Commits
0c237172e4
...
dev
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "scribe-plugin",
|
||||
"name": "grimoire-plugin",
|
||||
"owner": { "name": "Bryan Van Deusen" },
|
||||
"description": "Scribe ships its own Claude Code plugin from this repo, versioned in lockstep with the app + the /api/plugin/context contract.",
|
||||
"description": "Grimoire ships its own Claude Code plugin from this repo, versioned in lockstep with the app + the /api/plugin/context contract.",
|
||||
"plugins": [
|
||||
{
|
||||
"name": "scribe",
|
||||
"name": "grimoire",
|
||||
"source": "./plugin",
|
||||
"description": "Scribe second brain: MCP tools + session-start push channel + universal process-skills."
|
||||
"description": "Grimoire second brain: MCP tools + session-start push channel + universal process-skills."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+7
-2
@@ -1,4 +1,9 @@
|
||||
# An install created before the rename from Scribe keeps its database by
|
||||
# setting these, and PGDATA_VOLUME, to the names it was created with.
|
||||
POSTGRES_USER=fabled
|
||||
POSTGRES_PASSWORD=fabled
|
||||
POSTGRES_DB=scribe
|
||||
SECRET_KEY=dev-secret-change-me
|
||||
POSTGRES_DB=grimoire
|
||||
# PGDATA_VOLUME=grimoire_pgdata
|
||||
# Grimoire keeps its own SECRET_KEY in the /data volume. Set one here only to
|
||||
# use a key you manage yourself; it then wins over the kept one.
|
||||
# SECRET_KEY=
|
||||
|
||||
+10
-12
@@ -55,7 +55,7 @@ permissions:
|
||||
|
||||
env:
|
||||
REGISTRY: git.fabledsword.com
|
||||
IMAGE: git.fabledsword.com/bvandeusen/fabledscribe
|
||||
IMAGE: git.fabledsword.com/bvandeusen/fabledgrimoire
|
||||
|
||||
jobs:
|
||||
typecheck:
|
||||
@@ -99,7 +99,7 @@ jobs:
|
||||
# version check diffs two trees, so a depth-1 fetch of main is enough.
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
# Per-job rather than in the image: Scribe is shellcheck's only consumer.
|
||||
# Per-job rather than in the image: Grimoire is shellcheck's only consumer.
|
||||
# Install nothing else here; the hooks use only POSIX tools, and the
|
||||
# smoke test runs on a bare image to prove it.
|
||||
- name: Install shell tooling
|
||||
@@ -193,11 +193,11 @@ jobs:
|
||||
# Same image as the shipped compose files: Postgres 17 with pgvector.
|
||||
image: pgvector/pgvector:pg17
|
||||
env:
|
||||
POSTGRES_USER: scribe
|
||||
POSTGRES_USER: grimoire
|
||||
POSTGRES_PASSWORD: ci_integration
|
||||
POSTGRES_DB: scribe_test
|
||||
POSTGRES_DB: grimoire_test
|
||||
options: >-
|
||||
--health-cmd "pg_isready -U scribe"
|
||||
--health-cmd "pg_isready -U grimoire"
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 10
|
||||
@@ -235,7 +235,7 @@ jobs:
|
||||
test "$(printf '%s\n' "$PG" | grep -c .)" -eq 1
|
||||
PG_IP=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$PG")
|
||||
test -n "$PG_IP"
|
||||
export DATABASE_URL="postgresql+asyncpg://scribe:ci_integration@${PG_IP}:5432/scribe_test"
|
||||
export DATABASE_URL="postgresql+asyncpg://grimoire:ci_integration@${PG_IP}:5432/grimoire_test"
|
||||
# Wait for Postgres. The run: shell is dash (no bash /dev/tcp), so use Python.
|
||||
/opt/venv/bin/python - "$PG_IP" <<'PY'
|
||||
import socket, sys, time
|
||||
@@ -315,12 +315,10 @@ jobs:
|
||||
echo "build_key=$BUILD_KEY" >> $GITHUB_OUTPUT
|
||||
echo "channel=$CHANNEL" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Free disk space
|
||||
# Prunes unused containers and images, and caps the BuildKit cache at
|
||||
# 5 GB so the package caches survive but old layers don't pile up.
|
||||
run: |
|
||||
docker system prune -af || true
|
||||
docker builder prune --keep-storage 5g -f || true
|
||||
# No disk-space prune step: the runner's Docker daemon is shared, and
|
||||
# `docker system prune -af` here deleted images other repos' jobs had
|
||||
# just pulled and were about to start (CI-runner #16). The runner host
|
||||
# prunes nightly instead.
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
+6
-2
@@ -31,7 +31,7 @@ RUN --mount=type=cache,target=/root/.cache/uv \
|
||||
# alembic and hypercorn in CMD have to be found there.
|
||||
ENV PATH="/app/.venv/bin:$PATH"
|
||||
|
||||
COPY --from=build-frontend /build/dist/ src/scribe/static/
|
||||
COPY --from=build-frontend /build/dist/ src/grimoire/static/
|
||||
COPY alembic.ini .
|
||||
COPY alembic/ alembic/
|
||||
|
||||
@@ -56,5 +56,9 @@ ENV APP_BUILD_KEY=$BUILD_KEY
|
||||
ENV APP_CHANNEL=$BUILD_CHANNEL
|
||||
ENV APP_COMMIT=$BUILD_COMMIT
|
||||
|
||||
# The instance's own SECRET_KEY, the model cache and diagnostics live here.
|
||||
# Mount a volume on it so they outlive the container.
|
||||
RUN mkdir -p /data
|
||||
|
||||
EXPOSE 5000
|
||||
CMD ["sh", "-c", "alembic upgrade head && hypercorn 'scribe.app:create_app()' --bind 0.0.0.0:5000 --keep-alive 600"]
|
||||
CMD ["sh", "-c", "alembic upgrade head && hypercorn 'grimoire.app:create_app()' --bind 0.0.0.0:5000 --keep-alive 600"]
|
||||
|
||||
@@ -0,0 +1,661 @@
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
@@ -3,22 +3,22 @@
|
||||
# --- Docker ---
|
||||
|
||||
build:
|
||||
docker compose build
|
||||
docker compose -f docker-compose.dev.yml build
|
||||
|
||||
up:
|
||||
docker compose up -d
|
||||
docker compose -f docker-compose.dev.yml up -d
|
||||
|
||||
down:
|
||||
docker compose down
|
||||
docker compose -f docker-compose.dev.yml down
|
||||
|
||||
logs:
|
||||
docker compose logs -f
|
||||
docker compose -f docker-compose.dev.yml logs -f
|
||||
|
||||
health:
|
||||
curl -s http://localhost:5000/api/health
|
||||
|
||||
migrate:
|
||||
docker compose exec app alembic upgrade head
|
||||
docker compose -f docker-compose.dev.yml exec app alembic upgrade head
|
||||
|
||||
# --- Local dev (requires: uv sync --extra dev and cd frontend && npm ci) ---
|
||||
|
||||
|
||||
@@ -1,42 +1,150 @@
|
||||
# Fabled Scribe
|
||||
<p align="center">
|
||||
<img src="frontend/public/favicon.svg" width="96" alt="">
|
||||
</p>
|
||||
|
||||
A self-hosted work system-of-record for software projects, built to be driven by Claude Code. Notes, tasks, issues, projects, milestones, rules, and stored processes — reachable from Claude via a built-in MCP endpoint and a bundled Claude Code plugin, with a clean web UI for humans. No in-app LLM; Claude is the sole assistant.
|
||||
<h1 align="center">Fabled Grimoire</h1>
|
||||
|
||||
<p align="center">A self-hosted system of record for the work you do with Claude Code.</p>
|
||||
|
||||
Claude records what it learned, decided and built in Grimoire, and that record reaches the next session, so a solution is recalled instead of reinvented. Notes, tasks, plans, rules, lessons and reusable code live in one place. Claude reaches them through a built-in MCP endpoint and the bundled Claude Code plugin, and people read and edit the same records in the web UI. There is no language model inside Grimoire: Claude is the only assistant.
|
||||
|
||||
## Deploy
|
||||
|
||||
You need Docker with Compose. Save this as `docker-compose.yml` (it is the [`docker-compose.yml`](docker-compose.yml) in this repository), then run `docker compose up -d` beside it:
|
||||
|
||||
```yaml
|
||||
# Fabled Grimoire: the app and its database.
|
||||
# docker compose up -d
|
||||
# Then open http://localhost:5000. The first account registered becomes admin.
|
||||
#
|
||||
# Keep both volumes. app_data holds the instance's own SECRET_KEY, which seals
|
||||
# saved credentials; pgdata holds everything else.
|
||||
|
||||
services:
|
||||
app:
|
||||
image: git.fabledsword.com/bvandeusen/fabledgrimoire:latest
|
||||
ports:
|
||||
- "5000:5000"
|
||||
environment:
|
||||
DATABASE_URL: "postgresql+asyncpg://${POSTGRES_USER:-grimoire}:${POSTGRES_PASSWORD:-grimoire}@db:5432/${POSTGRES_DB:-grimoire}"
|
||||
LOG_LEVEL: "${LOG_LEVEL:-INFO}"
|
||||
# Port 5000 is published straight to clients, so no proxy is trusted.
|
||||
# Behind a reverse proxy, set 1 (one per proxy) and BASE_URL:
|
||||
TRUSTED_PROXY_HOPS: "0"
|
||||
# BASE_URL: "https://grimoire.example.com"
|
||||
volumes:
|
||||
- app_data:/data
|
||||
depends_on:
|
||||
- db
|
||||
restart: unless-stopped
|
||||
|
||||
db:
|
||||
# Postgres 17 with the vector extension Grimoire needs.
|
||||
image: pgvector/pgvector:pg17
|
||||
stop_grace_period: 120s
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
environment:
|
||||
POSTGRES_USER: ${POSTGRES_USER:-grimoire}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-grimoire}
|
||||
POSTGRES_DB: ${POSTGRES_DB:-grimoire}
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
app_data:
|
||||
pgdata:
|
||||
```
|
||||
|
||||
Open `http://localhost:5000`. The first account you register becomes the admin.
|
||||
|
||||
Nothing has to be set first. On its first start Grimoire makes its own `SECRET_KEY` and keeps it in the `app_data` volume, so restarts, reboots and updates all use the same key.
|
||||
|
||||
To connect Claude, create an API key under **Settings → MCP Access**, then in Claude Code:
|
||||
|
||||
```
|
||||
/plugin marketplace add https://git.fabledsword.com/bvandeusen/FabledGrimoire.git
|
||||
/plugin install grimoire@grimoire-plugin
|
||||
```
|
||||
|
||||
The plugin asks for your Grimoire URL and the API key. Settings → MCP Access shows the exact commands for your instance.
|
||||
|
||||
## Configuration
|
||||
|
||||
Everything is optional. Set these in the environment or in a `.env` file beside the compose file:
|
||||
|
||||
| Variable | When | What it does |
|
||||
|----------|------|--------------|
|
||||
| `BASE_URL` | Behind a proxy | The public URL, used in email links and SSO redirects |
|
||||
| `TRUSTED_PROXY_HOPS` | Rarely | The starting count of reverse proxies in front of Grimoire (default `1`; the compose file above sets `0` because it publishes the port directly). Check and change it in **Settings → Config → Reverse proxy**, which shows what each count reads for your own request; behind Cloudflare it is usually `2` |
|
||||
| `POSTGRES_PASSWORD` | Before the first start | The database password (defaults to `grimoire`; the database is reachable only inside the compose network). Postgres takes it once, when the volume is created |
|
||||
| `SECRET_KEY` | Rarely | A key you manage yourself, instead of the one Grimoire keeps. It wins over the kept key; remove it to go back |
|
||||
|
||||
Everything else (registration, email, SSO, git forge connections, retrieval tuning, backups) is in **Settings**, or listed in [Configuration](docs/configuration.md).
|
||||
|
||||
## Setup pitfalls
|
||||
|
||||
- **Keep both volumes.** `pgdata` holds everything Grimoire stores. `app_data` holds the instance's key, and forge tokens and the backup bucket's secret are sealed under it. If the key is lost, Settings asks you to enter those credentials again; nothing else is lost.
|
||||
- **The embedding model downloads on first use**, into the `app_data` volume. Without internet access, semantic search falls back to keyword search until the model is in place.
|
||||
- **Behind a reverse proxy, turn off response buffering** for `/mcp`, because MCP responses stream. [Configuration](docs/configuration.md#behind-a-reverse-proxy) has an Nginx example.
|
||||
|
||||
## Updating and backups
|
||||
|
||||
```bash
|
||||
docker compose pull
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
Database migrations run when the app starts, and the key stays in its volume. Claude Code updates the plugin from its marketplace; run `/reload-plugins` after an update.
|
||||
|
||||
**Settings → Data → Scheduled backups** writes a daily backup of the database to a folder or an S3-compatible bucket, and Settings → Data → Restore from Backup rolls the instance back to one, or brings a new instance back from it. See [Backups](docs/backups.md).
|
||||
|
||||
## Features
|
||||
|
||||
Notes, tasks and issues; projects with milestones as plans; rules that reach Claude when its work matches them or at a moment of work; lessons, snippets of prior art and a ledger of the code they account for; design systems; ideas shared across projects on the same platform; and stored processes. Semantic search feeds both Claude's recall and the plugin's context injection. A knowledge graph, sharing with users and groups, and scheduled backups round it out. See [Features](docs/features.md).
|
||||
**Working with Claude**
|
||||
- Claude enters a project and gets a short primer: its goal, current plans, open tasks and areas.
|
||||
- Your rules reach Claude when its work matches them, or at a moment of work such as finishing a task or pushing code.
|
||||
- Before Claude writes a file or runs a command, the plugin shows it the rules, lessons and reusable code that may apply.
|
||||
- Stored procedures, such as a drift audit, become Claude Code skills.
|
||||
|
||||
## Quick Start
|
||||
**Recording the work**
|
||||
- Notes in a Markdown editor, with wikilinks, tags and version history.
|
||||
- Tasks, issues and time-boxed investigations, with work logs that the next session reads first.
|
||||
- Plans as milestones: a design plus ordered steps.
|
||||
- Lessons learned the hard way, linked to the rules they illustrate.
|
||||
- Duplicate checks, so a new record that matches an existing one updates it instead.
|
||||
|
||||
**Prerequisites:** Docker and Docker Compose. No GPU or local model needed — Claude is the sole assistant, reached over MCP.
|
||||
**Reusing code**
|
||||
- Snippets of reusable code, each with when to reach for it and where it lives in the repo.
|
||||
- With a git forge connected, Grimoire tracks every function and style in your repos and shows which ones are still unaccounted for.
|
||||
- Ideas shared across projects on the same platform, with each project's answer recorded.
|
||||
- Design systems with tokens, inheritance and a generated stylesheet.
|
||||
|
||||
Download [`docker-compose.quickstart.yml`](docker-compose.quickstart.yml) from this repo, then:
|
||||
**For people**
|
||||
- A web UI to browse, search and edit everything, plus a graph of how notes connect.
|
||||
- Sharing with users and groups, as viewer, editor or admin.
|
||||
- Email and password sign-in, optional SSO through OIDC, and invite links.
|
||||
- Personal export to Markdown or JSON, and admin backup and restore.
|
||||
|
||||
The full list is in [Features](docs/features.md).
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| What you see | Why | Fix |
|
||||
|--------------|-----|-----|
|
||||
| `SECRET_KEY is set to the public placeholder` at startup | `SECRET_KEY` is `dev-secret-change-me` or `change-me-in-production` | Remove `SECRET_KEY`, or set a real one |
|
||||
| Settings asks for forge tokens or the backup secret again | The instance's key changed: the `app_data` volume was replaced, or `SECRET_KEY` was set or removed | Enter them again, or restore the previous key |
|
||||
| `Can't locate revision identified by '…'` at startup | An older image is running against a database a newer one has already migrated | Run the newer image; migrations only move forward |
|
||||
| The plugin's tools are missing in Claude Code | The plugin isn't installed, or Claude Code hasn't reloaded it | `/plugin` to install or update it, then `/reload-plugins` |
|
||||
| Search finds only exact words | The embedding model hasn't downloaded yet | Give the app internet access once, or wait for the first download to finish |
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
# Optional but recommended — set a secret key
|
||||
export SECRET_KEY=your-random-secret-here
|
||||
|
||||
docker compose -f docker-compose.quickstart.yml up -d
|
||||
docker compose -f docker-compose.dev.yml up --build
|
||||
```
|
||||
|
||||
Open `http://localhost:5000`. The first user to register becomes admin. To connect Claude, create an API key under **Settings → MCP Access** and install the Claude Code plugin — see [API Keys & MCP](docs/api-keys-and-mcp.md).
|
||||
[Development](docs/development.md) covers checks, migrations, the plugin and CI. [Architecture](docs/architecture.md) describes how it is built, and the [API reference](docs/api-reference.md) lists every REST endpoint.
|
||||
|
||||
> **Development:** To build from source, see [Development](docs/development.md).
|
||||
## Licence
|
||||
|
||||
## Documentation
|
||||
|
||||
| Doc | Contents |
|
||||
|-----|----------|
|
||||
| [Architecture](docs/architecture.md) | Stack, design decisions, data models, key services |
|
||||
| [Configuration](docs/configuration.md) | Environment variables, Docker Compose, production setup, security |
|
||||
| [Features](docs/features.md) | Detailed feature breakdown and keyboard shortcuts |
|
||||
| [Development](docs/development.md) | Dev workflow, CI/CD, migrations, release process |
|
||||
| [API Keys & MCP](docs/api-keys-and-mcp.md) | API keys and installing the Claude Code plugin |
|
||||
| [SSO / OAuth](docs/sso-oauth.md) | OIDC setup for Authentik, Keycloak, and other providers |
|
||||
| [Backups](docs/backups.md) | Scheduled backups to a folder or S3, and restoring from one |
|
||||
| [Design system](docs/design-system.md) | The FabledSword house style and how Scribe applies it |
|
||||
| [API Reference](docs/api-reference.md) | All REST API endpoints |
|
||||
|
||||
## License
|
||||
|
||||
This project is privately maintained.
|
||||
Fabled Grimoire is free software under the [GNU Affero General Public License v3.0 only](LICENSE) (`AGPL-3.0-only`). If you run a modified version for other people over a network, the licence requires you to offer them its source.
|
||||
|
||||
+3
-3
@@ -4,9 +4,9 @@ from logging.config import fileConfig
|
||||
from alembic import context
|
||||
from sqlalchemy.ext.asyncio import create_async_engine
|
||||
|
||||
from scribe.config import Config
|
||||
from scribe.models import Base
|
||||
from scribe.services.db_ready import wait_for_database
|
||||
from grimoire.config import Config
|
||||
from grimoire.models import Base
|
||||
from grimoire.services.db_ready import wait_for_database
|
||||
|
||||
config = context.config
|
||||
if config.config_file_name is not None:
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
"""replace events.end_dt with duration_minutes (Fable #160)
|
||||
"""replace events.end_dt with duration_minutes
|
||||
|
||||
Revision ID: 0043
|
||||
Revises: 0042
|
||||
Create Date: 2026-04-29
|
||||
|
||||
Structural fix for the "end before start" bug class observed on prod
|
||||
2026-04-29: an event landed with end_dt 32 days before start_dt due
|
||||
to a tool-call mishap, then disappeared from upcoming-list filters.
|
||||
Storing duration instead of end_dt makes the invalid state
|
||||
Structural fix for the "end before start" bug class: an event whose end_dt
|
||||
precedes its start_dt disappears from upcoming-list filters. Storing duration instead of end_dt makes the invalid state
|
||||
inexpressible at the schema level (duration_minutes >= 0).
|
||||
|
||||
Backfill rules:
|
||||
|
||||
@@ -5,7 +5,7 @@ Revises: 0043
|
||||
Create Date: 2026-05-13
|
||||
|
||||
Adds two columns to the ``notes`` table to support the task-as-durable-record
|
||||
design (spec 2026-05-13):
|
||||
design:
|
||||
|
||||
- ``description``: user-stated goal / initial context. Meaningful when
|
||||
``is_task=true``; left NULL on knowledge notes.
|
||||
|
||||
@@ -4,7 +4,7 @@ Revision ID: 0048
|
||||
Revises: 0047
|
||||
Create Date: 2026-05-22
|
||||
|
||||
Phase 2 of the conversation+curator architecture (Fable #172). The
|
||||
Part of the conversation+curator architecture. The
|
||||
scheduler runs every 15 minutes and processes any journal conversation
|
||||
with messages newer than its `last_curator_run_at` timestamp. NULL
|
||||
means "never run; process all of today on first sweep."
|
||||
|
||||
@@ -4,7 +4,7 @@ Revision ID: 0049
|
||||
Revises: 0048
|
||||
Create Date: 2026-05-22
|
||||
|
||||
Phase 3 of the conversation+curator architecture (Fable #172). The
|
||||
Part of the conversation+curator architecture. The
|
||||
curator's final summary line (≤ 240 chars) is persisted here so the
|
||||
NEXT chat turn can include it in the system prompt. This is the
|
||||
feedback loop that closes the architecture — without it, the chat
|
||||
|
||||
@@ -4,13 +4,11 @@ Revision ID: 0053
|
||||
Revises: 0052
|
||||
Create Date: 2026-05-27
|
||||
|
||||
Phase 9 of the MCP-first pivot. The Python models for these tables were
|
||||
deleted in Phase 8 (commit 91bafb6); this migration drops the orphan
|
||||
Part of the MCP-first pivot. The Python models for these tables are gone;
|
||||
this migration drops the orphan
|
||||
SQL tables and cleans up dead per-user setting rows.
|
||||
|
||||
Hard-cutover: existing rows in these tables are discarded. The
|
||||
accompanying spec at docs/superpowers/specs/2026-05-26-mcp-first-pivot-design.md
|
||||
explicitly accepted this loss.
|
||||
Hard-cutover: existing rows in these tables are discarded, deliberately.
|
||||
"""
|
||||
from alembic import op
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ Revision ID: 0054
|
||||
Revises: 0053
|
||||
Create Date: 2026-05-27
|
||||
|
||||
The image cache was wired into the LLM image-search tool (removed in Phase 8).
|
||||
The image cache was wired into the LLM image-search tool, which is gone.
|
||||
With no producer or consumer left, the table is dropped here.
|
||||
"""
|
||||
from alembic import op
|
||||
|
||||
@@ -6,8 +6,8 @@ Create Date: 2026-05-27
|
||||
|
||||
Adds the Scribe Rulebook hierarchy as a fourth top-level entity, sibling
|
||||
to Project. Rules carry a structural (statement, why, how_to_apply)
|
||||
triple. Projects subscribe to Rulebooks (many-to-many). See the design
|
||||
doc at docs/superpowers/specs/2026-05-27-scribe-rulebook-design.md.
|
||||
triple. Projects subscribe to Rulebooks (many-to-many; migration 0101
|
||||
later drops subscriptions).
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
@@ -4,7 +4,7 @@ Revision ID: 0066
|
||||
Revises: 0065
|
||||
Create Date: 2026-06-14
|
||||
|
||||
T3 of plan #819. The milestone becomes the plan container: its `body` holds
|
||||
The milestone becomes the plan container: its `body` holds
|
||||
the design/intent/purpose (markdown), `description` stays the one-liner, and
|
||||
individual steps live as first-class child tasks (milestone_id) instead of
|
||||
checkboxes crammed into a kind=plan task body. start_planning is reworked to
|
||||
|
||||
@@ -4,8 +4,7 @@ Revision ID: 0074
|
||||
Revises: 0073
|
||||
Create Date: 2026-07-31
|
||||
|
||||
The operator's shape for #254: prose doesn't live as one-offs, it lives centrally
|
||||
on the system. Two fields rather than one per category:
|
||||
Prose doesn't live as one-offs; it lives centrally on the system. Two fields rather than one per category:
|
||||
|
||||
design_systems.guidance the narrative a token table cannot hold — aesthetic,
|
||||
voice and tone, what is deliberately out of scope.
|
||||
@@ -17,7 +16,7 @@ on the system. Two fields rather than one per category:
|
||||
|
||||
Free-form rather than a column per category on purpose. A schema with `voice`,
|
||||
`aesthetic` and `scope` columns would bake one rulebook's table of contents into
|
||||
every install (rule #115), and the next install's design system would have three
|
||||
every install, and the next install's design system would have three
|
||||
empty columns and nowhere to put what it actually cares about.
|
||||
|
||||
Both nullable: a design system with no prose at all is complete, not a draft.
|
||||
|
||||
@@ -4,7 +4,7 @@ Revision ID: 0076
|
||||
Revises: 0075
|
||||
Create Date: 2026-08-07
|
||||
|
||||
Step 1 of milestone #278. Structure only — nothing reads or writes the new
|
||||
Structure only — nothing reads or writes the new
|
||||
table yet, and nothing behaves differently after this runs.
|
||||
|
||||
## What the table is for
|
||||
@@ -39,7 +39,7 @@ and not for notes, so the column looked like the notes-side half of that
|
||||
feature, modelled and abandoned.
|
||||
|
||||
It is dropped rather than repurposed for supersession, and the distinction is
|
||||
the point (#2483): consolidation folds several records into one survivor and
|
||||
the point: consolidation folds several records into one survivor and
|
||||
destroys the originals. Merging two snippets is lossless — one helper, several
|
||||
call sites. Folding two dev-logs means writing a summary and losing what each
|
||||
actually said. Supersession is the opposite act: both records survive, and the
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Chunked embeddings: one note_embeddings row per chunk (#280)
|
||||
"""Chunked embeddings: one note_embeddings row per chunk
|
||||
|
||||
Revision ID: 0077
|
||||
Revises: 0076
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Forge connections move to the user level (#2778)
|
||||
"""Forge connections move to the user level
|
||||
|
||||
Revision ID: 0078
|
||||
Revises: 0077
|
||||
@@ -12,7 +12,7 @@ keyring, with an optional per-project pin (projects.forge_connection_id).
|
||||
|
||||
The data move carries the existing admin config into a connection row for the
|
||||
first admin user (host parsed from the base URL), then deletes the old
|
||||
setting keys outright — no legacy dual-read (rule #22). The env-var channel
|
||||
setting keys outright — no legacy dual-read. The env-var channel
|
||||
(FORGE_KIND/FORGE_BASE_URL/FORGE_TOKEN) is untouched by this migration; it
|
||||
survives as an implicit keyring entry for admin users only.
|
||||
"""
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
"""The shape ledger: code_shapes (#2787, milestone 294)
|
||||
"""The shape ledger: code_shapes
|
||||
|
||||
Revision ID: 0079
|
||||
Revises: 0078
|
||||
Create Date: 2026-08-19
|
||||
|
||||
The accounting half of the pattern system (governing note 2786): the snippet
|
||||
The accounting half of the pattern system: the snippet
|
||||
library records canon (small); this table accounts for EVERY shape the
|
||||
coverage extractor finds in a bound repo (total). Rows arrive `unclassified`
|
||||
from the coverage sync (step 2) and gain judgments — canonical / instance /
|
||||
from the coverage sync and gain judgments — canonical / instance /
|
||||
variant / exempt — from audits, hooks, and the mechanical proposer.
|
||||
Unclassified IS the todo list.
|
||||
"""
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Shape fingerprints + the mechanical proposer's columns (#2792, milestone 294)
|
||||
"""Shape fingerprints + the mechanical proposer's columns
|
||||
|
||||
Revision ID: 0080
|
||||
Revises: 0079
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Shape history, recheck, and the divergence flag (#2793, milestone 294)
|
||||
"""Shape history, recheck, and the divergence flag
|
||||
|
||||
Revision ID: 0081
|
||||
Revises: 0080
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
"""Per-binding ref — the branch a project's ledger follows (#2873, milestone 294)
|
||||
"""Per-binding ref — the branch a project's ledger follows
|
||||
|
||||
Revision ID: 0082
|
||||
Revises: 0081
|
||||
Create Date: 2026-08-21
|
||||
|
||||
A repo binding used to imply the repo's default branch; the shape ledger
|
||||
therefore only saw work after a merge to main, while the operator's work
|
||||
lands on dev (rule 1). `ref` names the branch the coverage refresh reads —
|
||||
NULL keeps today's behaviour (the forge's default branch).
|
||||
Without a ref, a repo binding implies the repo's default branch, so the shape
|
||||
ledger only sees work after a merge — while a project may do its work on a
|
||||
dev branch. `ref` names the branch the coverage refresh reads; NULL means the
|
||||
forge's default branch.
|
||||
"""
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Exempt/variant reason codes — a small fixed catalogue beside the prose (#2874, milestone 294)
|
||||
"""Exempt/variant reason codes — a small fixed catalogue beside the prose
|
||||
|
||||
Revision ID: 0083
|
||||
Revises: 0082
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""code_shape_uses — consumption edges, separate from conformance (#2870, milestone 294)
|
||||
"""code_shape_uses — consumption edges, separate from conformance
|
||||
|
||||
Revision ID: 0084
|
||||
Revises: 0083
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Project inception: the decision record + always-on rulebook exclusions (milestone 297)
|
||||
"""Project inception: the decision record + always-on rulebook exclusions
|
||||
|
||||
Revision ID: 0085
|
||||
Revises: 0084
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""code_shape_consumers — the CSS consumer map (milestone 302, note 2917)
|
||||
"""code_shape_consumers — the CSS consumer map
|
||||
|
||||
Revision ID: 0086
|
||||
Revises: 0085
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
"""canonical_systems — the global area vocabulary, promoted from a constant
|
||||
to a table (milestone 307 step 1, decision note 3026)
|
||||
to a table
|
||||
|
||||
Revision ID: 0087
|
||||
Revises: 0086
|
||||
Create Date: 2026-08-26
|
||||
|
||||
The eight standard area names already existed as `STANDARD_SYSTEMS`, a tuple in
|
||||
services/systems.py that milestone 297 seeds into a project at inception. A
|
||||
services/systems.py that is seeded into a project at inception. A
|
||||
constant cannot be referenced: a rule that applies across projects has nothing
|
||||
to point at, because `systems.project_id` is NOT NULL and a family rule cannot
|
||||
be chained to one project's row. This makes the vocabulary a table so it can be
|
||||
@@ -35,7 +35,7 @@ depends_on = None
|
||||
# service computes (canonical_slug: lowercase, "&" -> "and", non-alphanumerics
|
||||
# collapsed to "-"). Charters stay generic on purpose: a project refines its
|
||||
# own System's description, never this one. Nothing here names an app, a repo,
|
||||
# a vendor or a house convention — the catalog ships to every install (rule 115).
|
||||
# a vendor or a house convention — the catalog ships to every install.
|
||||
_SEED = (
|
||||
("CI & Release", "ci-and-release",
|
||||
"How the project is verified and shipped: pipelines, runners, image/artifact builds, release tagging and rollback."),
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
"""rules gain a trigger, a tier, canon tags and typed edges (milestone 307
|
||||
step 3, decision note 3026)
|
||||
"""rules gain a trigger, a tier, canon tags and typed edges
|
||||
|
||||
Revision ID: 0088
|
||||
Revises: 0087
|
||||
@@ -31,8 +30,8 @@ down_revision = "0087"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
# Kept in one place so upgrade and the CHECK agree by construction (rule 36:
|
||||
# a whitelisted value means DROP + ADD CONSTRAINT in the same migration —
|
||||
# Kept in one place so upgrade and the CHECK agree by construction (a
|
||||
# whitelisted value means DROP + ADD CONSTRAINT in the same migration —
|
||||
# there is no prior constraint here, so the pair is created together).
|
||||
_TIERS = ("always_on", "conditional")
|
||||
_RELATION_KINDS = ("co_surfaces", "overrides", "elaborates")
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
"""rule_embeddings — rules become findable by meaning (milestone 307 step 4,
|
||||
decision note 3026)
|
||||
"""rule_embeddings — rules become findable by meaning
|
||||
|
||||
Revision ID: 0089
|
||||
Revises: 0088
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
"""a rule can carry its own check — verify_with, expires_when, verified_at
|
||||
(milestone 312 step 1)
|
||||
|
||||
Revision ID: 0090
|
||||
Revises: 0089
|
||||
@@ -11,10 +10,9 @@ know they did. A CONSTRAINT is a fact about someone else's software: a
|
||||
runner's shell, a bot's config, a tool that exists. Nobody is present when
|
||||
that goes false.
|
||||
|
||||
Milestone 307's rulebook audit found nine stale sites. Every one was a
|
||||
constraint; not one norm had rotted. One of them had been telling every
|
||||
session to skip database-backed tests for weeks while the integration lane
|
||||
sat green in the workflow.
|
||||
What goes stale in a rulebook is the constraints, not the norms — and a
|
||||
stale constraint keeps instructing every session that loads it, long after
|
||||
the world it described has moved.
|
||||
|
||||
Three nullable columns, so a rule can say how to check itself:
|
||||
|
||||
@@ -31,7 +29,7 @@ none of them. A null `verify_with` is not an omission — it is the honest
|
||||
marker of "this one is a decision, and there is nothing to go and check."
|
||||
That signal only works if the field stays empty wherever it belongs empty.
|
||||
|
||||
No CHECK constraint is involved, so rule 36 does not apply here. Nothing is
|
||||
No CHECK constraint is involved. Nothing is
|
||||
backfilled: a migration cannot invent a check any more than 0088 could
|
||||
invent a trigger.
|
||||
"""
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
"""task_kind gains 'spike' — the investigation, not the change
|
||||
(milestone 312 step 5)
|
||||
|
||||
Revision ID: 0091
|
||||
Revises: 0090
|
||||
@@ -12,12 +11,12 @@ end of it. "Find out whether the runner can be given a bash shell" is not
|
||||
work, and filing it as work makes a finished investigation look like an
|
||||
abandoned change.
|
||||
|
||||
It is the record a failed check asks for. Milestone 312 gave rules a
|
||||
It is the record a failed check asks for. Rules carry a
|
||||
`verify_with`; when one of those fails, the rule is wrong and the next move
|
||||
is often to go and find out what replaced it. `notes.arose_from_id` already
|
||||
exists (0065), so that constraint -> spike link needs no further schema.
|
||||
|
||||
Rule 36: `task_kind` is gated by a CHECK whitelist, so the value and the
|
||||
`task_kind` is gated by a CHECK whitelist, so the value and the
|
||||
widened constraint land in the SAME migration — DROP then ADD, exactly as
|
||||
0065 did when it introduced 'issue'. Adding the value and constraining it
|
||||
later leaves a window where the database accepts anything.
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
"""a note can carry its own check — verify_with, expires_when, verified_at
|
||||
(milestone 317 step 1)
|
||||
|
||||
Revision ID: 0092
|
||||
Revises: 0091
|
||||
@@ -32,8 +31,7 @@ Three nullable columns:
|
||||
|
||||
WHICH ROWS THESE ARE FOR. `notes` is one table holding notes, tasks,
|
||||
snippets and processes, so these columns land on all of them. Only non-task,
|
||||
non-snippet records are OFFERED them (milestone 317 decisions 1 and 2, gated
|
||||
at the service in step 2): a task's decay is its status, and a snippet
|
||||
non-snippet records are OFFERED them (gated at the service): a task's decay is its status, and a snippet
|
||||
already carries a richer, location-aware verdict in `data.verification`. The
|
||||
columns exist on the other rows and stay null there; a gate that lives in
|
||||
the schema would have meant a partial index or a CHECK across three columns
|
||||
@@ -44,7 +42,7 @@ value of the sweep is that its output is short. A null `verify_with` is not
|
||||
an omission; it is the honest marker of "this one is a decision, and there
|
||||
is nothing to go and check."
|
||||
|
||||
No CHECK constraint is involved, so rule 36 does not apply. Nothing is
|
||||
No CHECK constraint is involved. Nothing is
|
||||
backfilled: a migration cannot invent a check.
|
||||
"""
|
||||
import sqlalchemy as sa
|
||||
@@ -69,9 +67,8 @@ def upgrade() -> None:
|
||||
#
|
||||
# Still a sequential scan's job at this size, and an index on
|
||||
# (verified_at) filtered to `verify_with IS NOT NULL` would be maintained
|
||||
# on every note write to serve one operator-initiated query. If step 3's
|
||||
# live acceptance measures otherwise, add it there against a real plan
|
||||
# rather than guessing here.
|
||||
# on every note write to serve one operator-initiated query. If a real
|
||||
# plan says otherwise, add it then rather than guessing here.
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""rules gain an edit history — rule_versions (milestone 323 step 1)
|
||||
"""rules gain an edit history — rule_versions
|
||||
|
||||
Revision ID: 0093
|
||||
Revises: 0092
|
||||
@@ -7,12 +7,9 @@ Create Date: 2026-08-29
|
||||
The sibling `note_versions` has had for a long time. A note's every meaningful
|
||||
edit is snapshotted, and the design-system note calls that history "the
|
||||
changelog". A RULE — which binds behaviour on every session that loads it —
|
||||
had nothing: an edit destroyed what it used to say, with no record anywhere.
|
||||
|
||||
Rescoping rule 79 on 2026-08-29 is what surfaced it. The superseded statement
|
||||
had to be hand-copied into a task log to survive the edit (#3237), which is
|
||||
not a process, it is a person remembering. The more consequential record had
|
||||
the weaker protection.
|
||||
had nothing: an edit destroyed what it used to say, with no record anywhere,
|
||||
and keeping a superseded statement meant a person remembering to copy it
|
||||
somewhere. The more consequential record had the weaker protection.
|
||||
|
||||
Three things are deliberately NOT copied from note_versions, and each is a
|
||||
guard that exists there for a reason that does not hold here:
|
||||
@@ -31,7 +28,7 @@ guard that exists there for a reason that does not hold here:
|
||||
CASCADE: deleting a user must not erase the history of the rules they edited.
|
||||
The edit still happened and the rule still binds because of it.
|
||||
|
||||
No CHECK constraint, so rule 36 does not apply. Nothing is backfilled — a
|
||||
No CHECK constraint is involved. Nothing is backfilled — a
|
||||
migration cannot invent the text a rule used to have, and inventing "the
|
||||
current text, as of now" would be worse than an empty history, because it
|
||||
would look like a record of an edit that never occurred.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""add rule_usage_events — was a surfaced rule ever read? (milestone 333 step 1)
|
||||
"""add rule_usage_events — was a surfaced rule ever read?
|
||||
|
||||
Revision ID: 0094
|
||||
Revises: 0093
|
||||
@@ -9,18 +9,15 @@ table to arrive after `rule_embeddings` and `rule_versions` — each one added
|
||||
because the rule side kept inheriting machinery built for notes and getting
|
||||
the weaker version of it.
|
||||
|
||||
WHAT IT MEASURES. The write-path standing-rule arm is the only retrieval
|
||||
surface in Scribe whose usefulness cannot be observed, and — not coincidentally
|
||||
— the only one that has never declined to fire. Over 30 days it took 296 calls,
|
||||
returned something on every one, and cleared its threshold 100% of the time,
|
||||
while every other surface declines most of the time (#3311). That is either a
|
||||
perfectly tuned surface or a bar it cannot fail to clear, and `retrieval_logs`
|
||||
WHAT IT MEASURES. A standing-rule arm that returns something on every call
|
||||
and clears its threshold every time is either a perfectly tuned surface or a
|
||||
bar it cannot fail to clear, and `retrieval_logs`
|
||||
cannot tell them apart: it records what the ranker scored, never whether the
|
||||
hint was any use.
|
||||
|
||||
WHY NOT A rule_id COLUMN ON note_usage_events. The row shares no note-specific
|
||||
fields and the aggregate readout is the same shape, which is the strongest case
|
||||
for sharing that note #3163 admits. What decides against it is identity at
|
||||
for sharing there is. What decides against it is identity at
|
||||
RESTORE: `note_usage_events`'s importer maps `note_id` through `note_id_map`
|
||||
and drops what does not resolve. A rule id parked in that column would come
|
||||
back from a backup silently reattached to whatever note took that number —
|
||||
@@ -34,8 +31,8 @@ and dies with it; telemetry outlives the row it describes. Deleting a rule must
|
||||
not erase the evidence that it was surfaced forty times and opened never, since
|
||||
that evidence is exactly the case for having deleted it.
|
||||
|
||||
No CHECK on `event`, matching the note twin. Rule 36 governs adding a value to
|
||||
a column that is already gated; it does not require gating one that never was,
|
||||
No CHECK on `event`, matching the note twin. DROP + ADD governs adding a value
|
||||
to a column that is already gated; it does not require gating one that never was,
|
||||
and a two-member enum whose members are written by two functions in one module
|
||||
is not where that discipline earns its cost.
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""add retrieval_logs.suppressed_count — tell a ranker decline from a repeat (#3497)
|
||||
"""add retrieval_logs.suppressed_count — tell a ranker decline from a repeat
|
||||
|
||||
Revision ID: 0095
|
||||
Revises: 0094
|
||||
@@ -20,9 +20,8 @@ the dropped rows never come back and there is nothing to count.
|
||||
NULLABLE, AND THE NULL IS THE POINT. A surface that does not measure
|
||||
suppression stores NULL, not 0, and the readout renders it as "not measured"
|
||||
rather than "none". Defaulting to 0 would make an unmeasured surface look like
|
||||
a perfectly clean one — the exact substitution of an artifact for a
|
||||
measurement that #3311 made and that #3497 exists to correct. Doing it again,
|
||||
in the migration that fixes it, would be its own small joke.
|
||||
a perfectly clean one — the substitution of an artifact for a measurement
|
||||
that this column exists to prevent.
|
||||
|
||||
No backfill for the same reason: existing rows genuinely do not know, and
|
||||
saying so is the honest state. `retrieval_logs` is not restored from backup,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""add retrieval_logs.best_available_score — the score the bar rejected (#3670)
|
||||
"""add retrieval_logs.best_available_score — the score the bar rejected
|
||||
|
||||
Revision ID: 0096
|
||||
Revises: 0095
|
||||
@@ -34,7 +34,7 @@ NULLABLE, AND UNBACKFILLED, for the reason 0095 spells out: a row written
|
||||
before this shipped genuinely does not know what its best rejected candidate
|
||||
scored, and saying so is the honest state. A 0.0 default would read as "the
|
||||
corpus had nothing remotely relevant" — an artifact standing in for a
|
||||
measurement, which is the whole defect this milestone corrects.
|
||||
measurement, which is the defect this column corrects.
|
||||
|
||||
`retrieval_logs` is not restored from backup, so no importer changes.
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""add retrieval_logs.best_available_id — WHICH record the bar refused (#3807)
|
||||
"""add retrieval_logs.best_available_id — WHICH record the bar refused
|
||||
|
||||
Revision ID: 0097
|
||||
Revises: 0096
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""rules gain a `kind` — a preference is a rule that does not bind (#3849)
|
||||
"""rules gain a `kind` — a preference is a rule that does not bind
|
||||
|
||||
Revision ID: 0098
|
||||
Revises: 0097
|
||||
@@ -37,7 +37,7 @@ install upgrades and every existing row keeps the force it had. That is the
|
||||
same reasoning 0088 used for `tier`, and it is the reason both are safe to
|
||||
apply without reading the data first.
|
||||
|
||||
The CHECK is created with the column (rule 36: there is no prior constraint,
|
||||
The CHECK is created with the column (there is no prior constraint,
|
||||
so the pair is created together — a value added to it LATER does DROP + ADD
|
||||
in one migration).
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ Create Date: 2026-09-11
|
||||
`pre_tool_rule` retrieves against the RAW COMMAND TEXT and `write_path_rule`
|
||||
against the code being written, so whatever was on the command line or in the
|
||||
buffer is what `record_retrieval` wrote into `retrieval_logs.query`. A command
|
||||
that exported a token stored the token (#3925).
|
||||
that exported a token stored the token.
|
||||
|
||||
`services/retrieval_telemetry.scrub_secrets` closes that going forward — the
|
||||
value never reaches the column. It cannot reach BACKWARDS, and this does: it
|
||||
|
||||
@@ -4,14 +4,14 @@ Revision ID: 0100
|
||||
Revises: 0099
|
||||
Create Date: 2026-09-11
|
||||
|
||||
Milestone 394. Every rule now reaches a session by retrieval — because
|
||||
Every rule reaches a session by retrieval — because
|
||||
something it is about to do made the rule relevant — and the machinery that
|
||||
delivered rules unconditionally goes with it.
|
||||
|
||||
WHAT GOES, AND WHERE IT CAME FROM
|
||||
|
||||
- ``rules.tier`` and its ``ck_rules_tier`` CHECK (migration 0088). Dropping
|
||||
the column takes the constraint with it. Rule 36 is about ADDING a value to
|
||||
the column takes the constraint with it. DROP + ADD is about ADDING a value to
|
||||
a live whitelist, which needs DROP + ADD in the same migration; it does not
|
||||
speak to removing the column outright, and saying so here is cheaper than
|
||||
the next reader wondering whether it was forgotten.
|
||||
|
||||
@@ -4,12 +4,12 @@ Revision ID: 0101
|
||||
Revises: 0100
|
||||
Create Date: 2026-09-15
|
||||
|
||||
Milestone 414. A rule lives in a rulebook topic, where it is GLOBAL, or on one
|
||||
project, and retrieval reads that home directly (step 1). Subscriptions were
|
||||
the last thing that pretended a rulebook reached some projects and not others,
|
||||
and after milestone 394 they changed nothing a session received — only what a
|
||||
project's rule LISTING showed. Operator, 2026-09-15: "we have global and
|
||||
project scoped rules, we don't need the subscriptions now."
|
||||
A rule lives in a rulebook topic, where it is GLOBAL, or on one project, and
|
||||
retrieval reads that home directly. Subscriptions were the last thing that
|
||||
pretended a rulebook reached some projects and not others, and with no
|
||||
always-on tier they changed nothing a session received — only what a
|
||||
project's rule LISTING showed. Global and project-scoped rules cover what
|
||||
subscriptions were for.
|
||||
|
||||
WHAT GOES
|
||||
|
||||
@@ -19,8 +19,8 @@ WHAT GOES
|
||||
there is nothing to mute; a project that departs from a global rule writes
|
||||
a project rule with an ``overrides`` relation, which says why.
|
||||
- The ``subscribe_rulebooks`` key inside ``projects.inception.choices``, and
|
||||
the ``exclude_always_on_rulebooks`` key milestone 394 left behind in the
|
||||
same place. Both describe decisions that can no longer be made; a stored
|
||||
the ``exclude_always_on_rulebooks`` key the retired always-on tier left
|
||||
in the same place. Both describe decisions that can no longer be made; a stored
|
||||
record carrying them would be read back as a choice the product offers.
|
||||
|
||||
IRREVERSIBLE, AND THE DOWNGRADE SAYS SO
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""milestone_embeddings — a plan becomes findable by meaning (milestone 415)
|
||||
"""milestone_embeddings — a plan becomes findable by meaning
|
||||
|
||||
Revision ID: 0102
|
||||
Revises: 0101
|
||||
@@ -9,8 +9,8 @@ lives in — could not be found at all. So "is there already a plan for this?"
|
||||
had no tool, and a project whose roadmap was written as milestones had every
|
||||
later plan opened as a new milestone beside the one that already described it.
|
||||
|
||||
The sibling of rule_embeddings (0089), for the reasons its model docstring and
|
||||
note 3163 give. The vectors are DERIVED: nothing is backfilled here, the startup
|
||||
The sibling of rule_embeddings (0089), for the reasons its model docstring
|
||||
gives. The vectors are DERIVED: nothing is backfilled here, the startup
|
||||
backfill writes them.
|
||||
"""
|
||||
import sqlalchemy as sa
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
"""retrieval_tuning_events — why a floor is where it is (#4102)
|
||||
"""retrieval_tuning_events — why a floor is where it is
|
||||
|
||||
Revision ID: 0103
|
||||
Revises: 0102
|
||||
Create Date: 2026-09-17
|
||||
|
||||
Milestone 416 stops shipping similarity thresholds as values somebody has to
|
||||
defend, and hands the adjustment to the model that reads the surface's own
|
||||
telemetry. The operator's decision:
|
||||
|
||||
"the floor should be chosen and adjusted by the model using it… the user
|
||||
should be able to touch it but the model should be the thing handling it 9
|
||||
times out of 10."
|
||||
Similarity thresholds are not values somebody has to defend: the model that
|
||||
reads the surface's own telemetry adjusts them, and the operator can still
|
||||
touch them.
|
||||
|
||||
The number itself already has a home — the generic settings table. What has no
|
||||
home is the ARGUMENT, and once the values move on their own the argument is the
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"""retrieval_tuning_events carries the space each number was measured in (#4104)
|
||||
"""retrieval_tuning_events carries the space each number was measured in
|
||||
|
||||
Revision ID: 0104
|
||||
Revises: 0103
|
||||
Create Date: 2026-09-17
|
||||
|
||||
Milestone 416 step 6. A retrieval floor is a cosine distance in ONE embedding
|
||||
A retrieval floor is a cosine distance in ONE embedding
|
||||
model's geometry, computed over documents cut one particular way. Change the
|
||||
model and every score moves at once; change the chunker and the same record
|
||||
embeds different text. Either way a number chosen before the change is a
|
||||
@@ -16,7 +16,7 @@ merely unknown.
|
||||
the startup backfill re-embeds precisely what is stale instead of wiping the
|
||||
table. These two columns are that idea applied to the tuned numbers.
|
||||
|
||||
TWO COLUMNS, NOT ONE (rule 149). A reader has to be able to say WHICH half
|
||||
TWO COLUMNS, NOT ONE. A reader has to be able to say WHICH half
|
||||
moved: a new embedding model and a re-cut document shape invalidate the same
|
||||
numbers for different reasons, and a fused `"<model>@<n>"` could only report
|
||||
that something changed.
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
"""note_usage_events records WHICH project the reader was in (#4196, #3735)
|
||||
"""note_usage_events records WHICH project the reader was in
|
||||
|
||||
Revision ID: 0105
|
||||
Revises: 0104
|
||||
Create Date: 2026-09-20
|
||||
|
||||
Milestone 385. `RetrievalLog` has carried `project_id` from the start, so
|
||||
`RetrievalLog` has carried `project_id` from the start, so
|
||||
"this record was SURFACED on project B" has always been answerable. The usage
|
||||
table never had one, so "this record was OPENED on project B" never was — and
|
||||
the two cannot be joined to recover it, because there is no session identity
|
||||
server-side (see the NoteUsageEvent docstring, which rules that out on
|
||||
purpose).
|
||||
|
||||
That gap sits exactly on the question milestone 385 exists to answer. A
|
||||
lesson's whole claim is that it reaches a session on a project it was not
|
||||
written on; step 8's acceptance is "retrieved on a different project, at the
|
||||
moment it applies, AND opened". Surfacing cross-project: answerable. Opening:
|
||||
That gap sits exactly on the question lessons exist to answer. A lesson's
|
||||
whole claim is that it reaches a session on a project it was not written on:
|
||||
"retrieved on a different project, at the moment it applies, AND opened". Surfacing cross-project: answerable. Opening:
|
||||
answerable. Both at once: not, until this column.
|
||||
|
||||
WHICH PROJECT THIS IS, because the name is ambiguous and the wrong reading
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"""rule_usage_events carry an outcome, not just a read (#4212, milestone 419)
|
||||
"""rule_usage_events carry an outcome, not just a read
|
||||
|
||||
Revision ID: 0106
|
||||
Revises: 0105
|
||||
Create Date: 2026-09-20
|
||||
|
||||
Milestone 419's first step. `rule_usage_events` can say a rule was SURFACED
|
||||
`rule_usage_events` can say a rule was SURFACED
|
||||
and that it was PULLED. It cannot say what happened next, so these two
|
||||
sessions leave identical telemetry:
|
||||
|
||||
@@ -12,16 +12,14 @@ sessions leave identical telemetry:
|
||||
- a rule surfaced, opened, and silently ignored.
|
||||
|
||||
The second is the more urgent by a distance, and it is the one the readout
|
||||
cannot name. That is the whole of what this milestone is about, measured on a
|
||||
session where three of seven misses were caught by the operator and none by
|
||||
the system.
|
||||
cannot name, and it is what an outcome column is for.
|
||||
|
||||
TWO NEW EVENT VALUES, AND NO CHECK MIGRATION. `event` was created in 0094 as
|
||||
plain `sa.Text()` with no constraint — checked in the migration itself, not
|
||||
assumed from the model — so `applied` and `departed` join `surfaced` and
|
||||
`pulled` without a DROP/ADD pair. Rule 36 governs CHECK-whitelisted columns
|
||||
`pulled` without a DROP/ADD pair. That pair is for CHECK-whitelisted columns
|
||||
and this is not one; noted explicitly because the next reader will reach for
|
||||
rule 36 here, and should be able to see in one place why it does not bite.
|
||||
it here, and should be able to see in one place why it does not apply.
|
||||
|
||||
THE THIRD STATE IS DERIVED, AND THAT IS NOT A SHORTCUT. Read-and-silently-
|
||||
unchanged is the absence of an outcome, and it has to be: an agent that knew
|
||||
@@ -29,8 +27,8 @@ it was ignoring a rule would not be ignoring it. There is no honest way to ask
|
||||
for that event, so nothing here tries. `applied` and `departed` are reported;
|
||||
the third state is what is left over when a rule was pulled and neither
|
||||
arrived. A schema that offered an `ignored` value would collect nothing and
|
||||
read as though it had measured something, which is the #3311 failure — a
|
||||
statistic that cannot vary being mistaken for a finding.
|
||||
read as though it had measured something — a statistic that cannot vary
|
||||
being mistaken for a finding.
|
||||
|
||||
`detail` CARRIES THE WHY OF A DEPARTURE, and is the reason this is a column
|
||||
rather than two more bare event strings. A departure without its reason is
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""system_embeddings — a charter becomes an ANSWER, not just a filter (#4251)
|
||||
"""system_embeddings — a charter becomes an ANSWER, not just a filter
|
||||
|
||||
Revision ID: 0107
|
||||
Revises: 0106
|
||||
@@ -13,8 +13,7 @@ to one, and an agent asking where a record belonged had to read every charter
|
||||
or guess.
|
||||
|
||||
The fourth sibling of note_embeddings (0067), rule_embeddings (0089) and
|
||||
milestone_embeddings (0102), and for the same reason note 3163 gives about the
|
||||
third: the row could be shared, the search cannot. "Where does this belong?"
|
||||
milestone_embeddings (0102), and for the same reason as the third: the row could be shared, the search cannot. "Where does this belong?"
|
||||
is a different question from "what prior art is there?", and no note or rule
|
||||
search can answer it, because a charter is not a note.
|
||||
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
"""trigger_leaves_the_stored_title — a snippet's and lesson's title is its name (milestone 427)
|
||||
"""trigger_leaves_the_stored_title — a snippet's and lesson's title is its name
|
||||
|
||||
Revision ID: 0108
|
||||
Revises: 0107
|
||||
Create Date: 2026-09-23
|
||||
|
||||
Snippets and lessons stored their title as `subject — trigger`, because the
|
||||
join is what makes these kinds rank on the situation they apply to (#2485) and
|
||||
join is what makes these kinds rank on the situation they apply to and
|
||||
the stored title WAS the embedded one. Every surface that shows a title then
|
||||
showed the trigger too: menu lines, lists and search rows ran to 1,500–3,000
|
||||
characters, and an injected repeat spent all of it again.
|
||||
|
||||
The trigger's home is `data` (decision #4157), and since this milestone the
|
||||
join happens at embed time (`embeddings.document_title`). This revision
|
||||
The trigger's home is `data`, and the join happens at embed time (`embeddings.document_title`). This revision
|
||||
rewrites the rows already stored.
|
||||
|
||||
EXACT-SUFFIX, READ FROM THE ROW'S OWN MIRROR. A title is only rewritten when
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""embeddings_record_their_model — a vector says whose space it lives in (#4132)
|
||||
"""embeddings_record_their_model — a vector says whose space it lives in
|
||||
|
||||
Revision ID: 0109
|
||||
Revises: 0108
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""task_claims — a task can say a session is working it (milestone 381 step 2)
|
||||
"""task_claims — a task can say a session is working it
|
||||
|
||||
Revision ID: 0110
|
||||
Revises: 0109
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
"""lesson_rule_links — a lesson points at the rule it is an instance of
|
||||
(milestone 440 step 1, #4196)
|
||||
|
||||
Revision ID: 0111
|
||||
Revises: 0110
|
||||
@@ -21,8 +20,8 @@ down_revision = "0110"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
# One place, so the CHECK and the model's LINK_STATES cannot drift (rule 36:
|
||||
# a new value later means DROP + ADD CONSTRAINT in the same migration).
|
||||
# One place, so the CHECK and the model's LINK_STATES cannot drift (a new
|
||||
# value later means DROP + ADD CONSTRAINT in the same migration).
|
||||
_STATES = ("suggested", "confirmed", "rejected")
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
"""lesson_no_rule — "no rule fits" is a recorded answer (milestone 440 step 2,
|
||||
#4631)
|
||||
"""lesson_no_rule — "no rule fits" is a recorded answer
|
||||
|
||||
Revision ID: 0112
|
||||
Revises: 0111
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
"""system_path_patterns — a System names the files that are its area
|
||||
(milestone 444 step 3, #4756)
|
||||
|
||||
Revision ID: 0113
|
||||
Revises: 0112
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""system_usage_events — were an area's rulings read once its files were
|
||||
touched? (milestone 444 step 4, #4757)
|
||||
touched?
|
||||
|
||||
Revision ID: 0114
|
||||
Revises: 0113
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
"""retrieval_judgments — a reviewer's verdict on each line of a logged menu
|
||||
(#4772)
|
||||
|
||||
Revision ID: 0115
|
||||
Revises: 0114
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""moment_mappings — an install's own corrections to which actions reach which
|
||||
moment (milestone 458 step 2)
|
||||
moment
|
||||
|
||||
Revision ID: 0116
|
||||
Revises: 0115
|
||||
@@ -8,7 +8,7 @@ Create Date: 2026-10-05
|
||||
The moments and their default actions are code; this table holds what an
|
||||
install adds ("make ship" is a deliver here) and the defaults it switches off.
|
||||
User-owned configuration, so it cascades with the user. No CHECK on `effect`,
|
||||
which the service validates (rule 36).
|
||||
which the service validates.
|
||||
"""
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""rule_moments — the moments of work a rule arrives at (milestone 458 step 3)
|
||||
"""rule_moments — the moments of work a rule arrives at
|
||||
|
||||
Revision ID: 0117
|
||||
Revises: 0116
|
||||
@@ -28,7 +28,7 @@ def upgrade() -> None:
|
||||
sa.Column("moment", sa.Text(), primary_key=True),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), nullable=True),
|
||||
)
|
||||
# The delivery read (step 4) asks "which rules are on this moment?", the
|
||||
# The delivery read asks "which rules are on this moment?", the
|
||||
# opposite direction from the primary key.
|
||||
op.create_index("ix_rule_moments_moment", "rule_moments", ["moment"])
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
"""rule_moment_judgments — whether a rule belongs on a moment, and who said so
|
||||
(milestone 458 step 7, #4925)
|
||||
|
||||
Revision ID: 0118
|
||||
Revises: 0117
|
||||
@@ -23,8 +22,8 @@ branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
# One place each, so the CHECKs and the model's JUDGMENT_STATES / SOURCES
|
||||
# cannot drift (rule 36: a new value later means DROP + ADD CONSTRAINT in the
|
||||
# same migration).
|
||||
# cannot drift (a new value later means DROP + ADD CONSTRAINT in the same
|
||||
# migration).
|
||||
_STATES = ("suggested", "confirmed", "rejected")
|
||||
_SOURCES = ("pass", "signal", "edit")
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""rule_moment_judgments.misfire — a mount reported as arriving where it does
|
||||
not apply (milestone 458 step 7b, #4955)
|
||||
not apply
|
||||
|
||||
Revision ID: 0119
|
||||
Revises: 0118
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""family canon — platforms, family ideas, the adoption ledger and its decision
|
||||
log (milestone 463 step 1)
|
||||
log
|
||||
|
||||
Revision ID: 0120
|
||||
Revises: 0119
|
||||
@@ -13,7 +13,7 @@ reason), or owed. These tables hold that.
|
||||
- `platforms` — a GLOBAL catalog, the canonical_systems shape: no owner, slug
|
||||
is the match key. Seeded with generic technology names only. Nothing here
|
||||
names an app, a repo or a house convention: the catalog ships to every
|
||||
install (rule 115).
|
||||
install.
|
||||
- `project_platforms` — which platforms a project is (declared / detected /
|
||||
rejected). Membership is what makes "is this in family?" a lookup.
|
||||
- `family_ideas` — a NOTE's family state. No new record type: any notes row
|
||||
@@ -39,7 +39,7 @@ branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
# One place per whitelist, so each CHECK and the model's tuple cannot drift
|
||||
# (rule 36: a new value later means DROP + ADD CONSTRAINT in one migration).
|
||||
# (a new value later means DROP + ADD CONSTRAINT in one migration).
|
||||
_MEMBERSHIP = ("declared", "detected", "rejected")
|
||||
_IDEA_STATUSES = ("candidate", "canon", "retired")
|
||||
_ADOPTION_STATUSES = ("unassessed", "adopted", "variant", "exempt", "owed")
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""retire the report_preference arm's rows (milestone 500 step 4)
|
||||
"""retire the report_preference arm's rows
|
||||
|
||||
Revision ID: 0121
|
||||
Revises: 0120
|
||||
@@ -6,8 +6,8 @@ Create Date: 2026-10-09
|
||||
|
||||
The fixed-question arm that searched for completion-report preferences when a
|
||||
task closed is gone: the reply shapes and the preferences mounted beside them
|
||||
now arrive on the moments (milestone 500 step 3). Its rows go with it (rule
|
||||
22 — no legacy to preserve), because each one would otherwise mean something
|
||||
now arrive on the moments. Its rows go with it (there is no legacy to
|
||||
preserve), because each one would otherwise mean something
|
||||
different once the arm is no longer registered:
|
||||
|
||||
- `retrieval_logs` / `retrieval_judgments` — a source missing from the
|
||||
@@ -21,11 +21,10 @@ different once the arm is no longer registered:
|
||||
- `settings` — the arm's floor and budget keys, which nothing reads.
|
||||
|
||||
COST IS BOUNDED BY AN INDEX, NOT BY THE TABLE. A migration runs at boot
|
||||
against the live tables, which CI never has (lesson #5221). `retrieval_logs`
|
||||
against the live tables, which CI never has. `retrieval_logs`
|
||||
and `retrieval_judgments` are indexed on `source`. `rule_usage_events` is not,
|
||||
so its delete is also bounded by `created_at` (indexed): the arm shipped with
|
||||
milestone 409 step 4, decided 2026-09-14, so no row of it predates
|
||||
`ARM_BORN`, and the scan covers weeks rather than the table's whole history.
|
||||
so its delete is also bounded by `created_at` (indexed): no row of the arm
|
||||
predates `ARM_BORN`, and the scan covers weeks rather than the table's whole history.
|
||||
|
||||
Not reversible: the arm is not coming back, so neither are its rows.
|
||||
"""
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Seal forge-connection tokens at rest (milestone 523)
|
||||
"""Seal forge-connection tokens at rest
|
||||
|
||||
Revision ID: 0122
|
||||
Revises: 0121
|
||||
@@ -43,7 +43,7 @@ _LABEL_V1 = b"scribe.secrets.v1\x00"
|
||||
def _fernet():
|
||||
from cryptography.fernet import Fernet
|
||||
|
||||
from scribe.config import Config
|
||||
from grimoire.config import Config
|
||||
|
||||
digest = hashlib.sha256(_LABEL_V1 + Config.SECRET_KEY.encode()).digest()
|
||||
return Fernet(base64.urlsafe_b64encode(digest))
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
"""drop user_profiles
|
||||
|
||||
Revision ID: 0123
|
||||
Revises: 0122
|
||||
Create Date: 2026-10-22
|
||||
|
||||
The Profile settings page asked for a display name, job title, expertise,
|
||||
response style, tone, interests and a work schedule, and said an assistant
|
||||
used them to personalise chat and a daily journal. Nothing in the app read
|
||||
any of it: the page saved to this table and the backup carried it, and that
|
||||
was all. The page, its API and the backup section are gone, and the table
|
||||
goes with them.
|
||||
|
||||
Downgrade recreates the empty table, so the schema steps back cleanly; the
|
||||
rows themselves do not come back.
|
||||
"""
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
from sqlalchemy.dialects.postgresql import ARRAY, JSONB
|
||||
|
||||
revision = "0123"
|
||||
down_revision = "0122"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.drop_index("ix_user_profiles_user_id", table_name="user_profiles")
|
||||
op.drop_table("user_profiles")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.create_table(
|
||||
"user_profiles",
|
||||
sa.Column("id", sa.Integer(), primary_key=True),
|
||||
sa.Column(
|
||||
"user_id",
|
||||
sa.Integer(),
|
||||
sa.ForeignKey("users.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
unique=True,
|
||||
),
|
||||
sa.Column("display_name", sa.Text(), nullable=True),
|
||||
sa.Column("job_title", sa.Text(), nullable=True),
|
||||
sa.Column("industry", sa.Text(), nullable=True),
|
||||
sa.Column("expertise_level", sa.Text(), nullable=True),
|
||||
sa.Column("response_style", sa.Text(), nullable=True),
|
||||
sa.Column("tone", sa.Text(), nullable=True),
|
||||
sa.Column("interests", ARRAY(sa.Text()), nullable=True),
|
||||
sa.Column("work_schedule", JSONB(), nullable=True),
|
||||
sa.Column(
|
||||
"created_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.func.now(),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column(
|
||||
"updated_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.func.now(),
|
||||
nullable=False,
|
||||
),
|
||||
)
|
||||
op.create_index("ix_user_profiles_user_id", "user_profiles", ["user_id"], unique=True)
|
||||
@@ -0,0 +1,45 @@
|
||||
"""skill names follow the plugin's name
|
||||
|
||||
Revision ID: 0124
|
||||
Revises: 0123
|
||||
Create Date: 2026-10-22
|
||||
|
||||
The plugin is named `grimoire`, so its skills reach a session as
|
||||
`grimoire:<skill>` and stored processes as `grimoire-proc-<slug>`. A rule
|
||||
mounted on one of those skills, a judgment about that mount, and an install's
|
||||
own action mapping all store the skill's name, and a stored name the harness no
|
||||
longer sends never matches again. This rewrites the plugin's earlier prefixes,
|
||||
`scribe:` and `scribe-proc-`, to the current ones.
|
||||
|
||||
Cost: three small tables, each one row per mount, judgment or mapping.
|
||||
"""
|
||||
from alembic import op
|
||||
|
||||
revision = "0124"
|
||||
down_revision = "0123"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
_MOMENT_TABLES = ("rule_moments", "rule_moment_judgments", "moment_mappings")
|
||||
|
||||
|
||||
def _rename(old: str, new: str) -> None:
|
||||
for table in _MOMENT_TABLES:
|
||||
op.execute(
|
||||
f"UPDATE {table} SET moment = regexp_replace(moment, "
|
||||
f"'^skill\\.{old}(:|-proc-)', 'skill.{new}\\1') "
|
||||
f"WHERE moment ~ '^skill\\.{old}(:|-proc-)'"
|
||||
)
|
||||
op.execute(
|
||||
f'UPDATE moment_mappings SET "match" = regexp_replace("match", '
|
||||
f"'skill={old}(:|-proc-)', 'skill={new}\\1', 'g') "
|
||||
f"WHERE \"match\" ~ 'skill={old}(:|-proc-)'"
|
||||
)
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
_rename("scribe", "grimoire")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
_rename("grimoire", "scribe")
|
||||
@@ -0,0 +1,35 @@
|
||||
"""drop machine-written uses edges that cross projects
|
||||
|
||||
Revision ID: 0125
|
||||
Revises: 0124
|
||||
Create Date: 2026-10-26
|
||||
|
||||
A `uses` edge with basis `reference` is the proposer's by-name reading: the
|
||||
shape's body names a canon's symbol. A body never calls a function defined in
|
||||
another project's repository, so such an edge across projects is a common
|
||||
word matched, not a call. The proposer no longer writes them, and this removes
|
||||
the ones it wrote before. Edges a person or an audit judged keep their own
|
||||
basis and are left alone. Rows within one project are re-read on the next
|
||||
coverage refresh, which replaces their reference edges under the same rule.
|
||||
|
||||
Downgrade restores nothing: the removed edges were never facts.
|
||||
"""
|
||||
from alembic import op
|
||||
|
||||
revision = "0125"
|
||||
down_revision = "0124"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.execute(
|
||||
"DELETE FROM code_shape_uses u USING code_shapes s, notes n "
|
||||
"WHERE u.shape_id = s.id AND u.snippet_id = n.id "
|
||||
"AND u.basis = 'reference' "
|
||||
"AND n.project_id IS NOT NULL AND n.project_id <> s.project_id"
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
pass
|
||||
@@ -0,0 +1,34 @@
|
||||
"""drop write-path uses edges that cross projects
|
||||
|
||||
Revision ID: 0126
|
||||
Revises: 0125
|
||||
Create Date: 2026-10-26
|
||||
|
||||
The write path records a `uses` edge with basis `hook` when a session pulled a
|
||||
snippet and then wrote code naming its symbol. That is by-name evidence, like
|
||||
the proposer's `reference` edges 0125 removed, and a body never calls a
|
||||
function defined in another project's repository. The write path now applies
|
||||
the same by-name test, so it writes no such edge; this removes the ones it
|
||||
wrote before. Judged edges keep their own basis and are left alone.
|
||||
|
||||
Downgrade restores nothing: the removed edges were never facts.
|
||||
"""
|
||||
from alembic import op
|
||||
|
||||
revision = "0126"
|
||||
down_revision = "0125"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.execute(
|
||||
"DELETE FROM code_shape_uses u USING code_shapes s, notes n "
|
||||
"WHERE u.shape_id = s.id AND u.snippet_id = n.id "
|
||||
"AND u.basis = 'hook' "
|
||||
"AND n.project_id IS NOT NULL AND n.project_id <> s.project_id"
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
pass
|
||||
@@ -0,0 +1,39 @@
|
||||
"""instance identity — the id lineage a backup carries
|
||||
|
||||
Revision ID: 0127
|
||||
Revises: 0126
|
||||
Create Date: 2026-11-05
|
||||
|
||||
One row naming this database's id lineage. Every full backup carries it, a
|
||||
restore into a fresh instance adopts it (the restore keeps the original ids),
|
||||
and rolling a live instance back requires the backup's to match, so a backup's
|
||||
ids never land on unrelated rows. Made here with a random value, so every
|
||||
instance has one before its first account exists.
|
||||
"""
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision = "0127"
|
||||
down_revision = "0126"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.create_table(
|
||||
"instance_identity",
|
||||
sa.Column("id", sa.Integer(), primary_key=True),
|
||||
sa.Column("instance_id", sa.Text(), nullable=False),
|
||||
sa.Column(
|
||||
"created_at", sa.DateTime(timezone=True), nullable=False,
|
||||
server_default=sa.func.now(),
|
||||
),
|
||||
sa.CheckConstraint("id = 1", name="ck_instance_identity_single_row"),
|
||||
)
|
||||
op.execute(
|
||||
"INSERT INTO instance_identity (id, instance_id) VALUES (1, gen_random_uuid()::text)"
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_table("instance_identity")
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
# CI Requirements — FabledScribe
|
||||
# CI Requirements — FabledGrimoire
|
||||
|
||||
> Spec lives in [`docs/process.md`](https://git.fabledsword.com/bvandeusen/CI-runner/src/branch/main/docs/process.md)
|
||||
> in the CI-Runner repo.
|
||||
@@ -29,7 +29,7 @@ Anything CI installs at job time that isn't in the image. Promotion
|
||||
candidates if more than one project needs them.
|
||||
|
||||
- `shellcheck` — apt-installed in the **plugin** job, which lints the
|
||||
Claude Code hook scripts and runs their fail-open smoke test. Scribe is
|
||||
Claude Code hook scripts and runs their fail-open smoke test. Grimoire is
|
||||
its only consumer, so per `docs/process.md` it stays per-job. It is small
|
||||
(~20 MB) and a promotion candidate once another project lints shell.
|
||||
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
# Grimoire built from this checkout, for development:
|
||||
# docker compose -f docker-compose.dev.yml up --build
|
||||
|
||||
services:
|
||||
app:
|
||||
build: .
|
||||
ports:
|
||||
- "5000:5000"
|
||||
depends_on:
|
||||
- db
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- app_data:/data
|
||||
# Or a bind mount, for direct host access to the app's data:
|
||||
# - ./data:/data
|
||||
environment:
|
||||
DATABASE_URL: "postgresql+asyncpg://${POSTGRES_USER:-grimoire}:${POSTGRES_PASSWORD:-grimoire}@db:5432/${POSTGRES_DB:-grimoire}"
|
||||
# Optional: a SearXNG instance to list as the web-search backend in Integrations.
|
||||
# SEARXNG_URL: "http://searxng:8080"
|
||||
|
||||
db:
|
||||
# Postgres 17 with the vector extension Grimoire needs.
|
||||
image: pgvector/pgvector:pg17
|
||||
stop_grace_period: 120s
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
environment:
|
||||
POSTGRES_USER: ${POSTGRES_USER:-grimoire}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-grimoire}
|
||||
POSTGRES_DB: ${POSTGRES_DB:-grimoire}
|
||||
|
||||
volumes:
|
||||
pgdata:
|
||||
app_data:
|
||||
+18
-10
@@ -1,31 +1,39 @@
|
||||
services:
|
||||
app:
|
||||
image: git.fabledsword.com/bvandeusen/fabledscribe:latest
|
||||
image: git.fabledsword.com/bvandeusen/fabledgrimoire:latest
|
||||
environment:
|
||||
DATABASE_URL: "postgresql+asyncpg://scribe:${DB_PASSWORD}@db:5432/scribe"
|
||||
DATABASE_URL: "postgresql+asyncpg://${POSTGRES_USER:-grimoire}:${DB_PASSWORD}@db:5432/${POSTGRES_DB:-grimoire}"
|
||||
# A Swarm can start the app on another node, away from the key kept in
|
||||
# a node-local /data volume, so a stack sets its key itself (or mounts
|
||||
# a Docker secret and sets SECRET_KEY_FILE to its path).
|
||||
SECRET_KEY: "${SECRET_KEY}"
|
||||
LOG_LEVEL: "${LOG_LEVEL:-INFO}"
|
||||
TRUST_PROXY_HEADERS: "true"
|
||||
SECURE_COOKIES: "true"
|
||||
# Proxies in front of the app (Traefik = 1). The client address and
|
||||
# HTTPS are read from that far in from the right of X-Forwarded-*.
|
||||
TRUSTED_PROXY_HOPS: "1"
|
||||
volumes:
|
||||
- app_data:/data
|
||||
networks:
|
||||
- scribe_backend
|
||||
- backend
|
||||
|
||||
db:
|
||||
# Postgres 17 with the vector extension Scribe needs.
|
||||
# Postgres 17 with the vector extension Grimoire needs.
|
||||
image: pgvector/pgvector:pg17
|
||||
stop_grace_period: 120s
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
environment:
|
||||
POSTGRES_USER: scribe
|
||||
POSTGRES_USER: ${POSTGRES_USER:-grimoire}
|
||||
POSTGRES_PASSWORD: "${DB_PASSWORD}"
|
||||
POSTGRES_DB: scribe
|
||||
POSTGRES_DB: ${POSTGRES_DB:-grimoire}
|
||||
networks:
|
||||
- scribe_backend
|
||||
- backend
|
||||
|
||||
volumes:
|
||||
pgdata:
|
||||
name: ${PGDATA_VOLUME:-grimoire_pgdata}
|
||||
app_data:
|
||||
|
||||
networks:
|
||||
scribe_backend:
|
||||
backend:
|
||||
driver: overlay
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
# Fabled Scribe from the pre-built image:
|
||||
# docker compose -f docker-compose.quickstart.yml up -d
|
||||
# Then open http://localhost:5000 (the first account registered becomes admin)
|
||||
# and connect Claude from Settings → MCP Access.
|
||||
|
||||
services:
|
||||
app:
|
||||
image: git.fabledsword.com/bvandeusen/fabledscribe:latest
|
||||
ports:
|
||||
- "5000:5000"
|
||||
environment:
|
||||
DATABASE_URL: "postgresql+asyncpg://scribe:scribe@db:5432/scribe"
|
||||
# Set SECRET_KEY in the environment or a .env file beside this one.
|
||||
SECRET_KEY: "${SECRET_KEY:-change-me-in-production}"
|
||||
LOG_LEVEL: "${LOG_LEVEL:-INFO}"
|
||||
volumes:
|
||||
- app_data:/data
|
||||
depends_on:
|
||||
- db
|
||||
restart: unless-stopped
|
||||
|
||||
db:
|
||||
# Postgres 17 with the vector extension Scribe needs.
|
||||
image: pgvector/pgvector:pg17
|
||||
stop_grace_period: 120s
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
environment:
|
||||
POSTGRES_USER: scribe
|
||||
POSTGRES_PASSWORD: scribe
|
||||
POSTGRES_DB: scribe
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
app_data:
|
||||
pgdata:
|
||||
+23
-16
@@ -1,33 +1,40 @@
|
||||
# Fabled Grimoire: the app and its database.
|
||||
# docker compose up -d
|
||||
# Then open http://localhost:5000. The first account registered becomes admin.
|
||||
#
|
||||
# Keep both volumes. app_data holds the instance's own SECRET_KEY, which seals
|
||||
# saved credentials; pgdata holds everything else.
|
||||
|
||||
services:
|
||||
app:
|
||||
build: .
|
||||
image: git.fabledsword.com/bvandeusen/fabledgrimoire:latest
|
||||
ports:
|
||||
- "5000:5000"
|
||||
environment:
|
||||
DATABASE_URL: "postgresql+asyncpg://${POSTGRES_USER:-grimoire}:${POSTGRES_PASSWORD:-grimoire}@db:5432/${POSTGRES_DB:-grimoire}"
|
||||
LOG_LEVEL: "${LOG_LEVEL:-INFO}"
|
||||
# Port 5000 is published straight to clients, so no proxy is trusted.
|
||||
# Behind a reverse proxy, set 1 (one per proxy) and BASE_URL:
|
||||
TRUSTED_PROXY_HOPS: "0"
|
||||
# BASE_URL: "https://grimoire.example.com"
|
||||
volumes:
|
||||
- app_data:/data
|
||||
depends_on:
|
||||
- db
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- app_data:/data
|
||||
# Or a bind mount, for direct host access to the app's data:
|
||||
# - ./data:/data
|
||||
environment:
|
||||
DATABASE_URL: "postgresql+asyncpg://${POSTGRES_USER:-scribe}:${POSTGRES_PASSWORD:-scribe}@db:5432/${POSTGRES_DB:-scribe}"
|
||||
SECRET_KEY: "${SECRET_KEY:-dev-secret-change-me}"
|
||||
# Optional: a SearXNG instance to list as the web-search backend in Integrations.
|
||||
# SEARXNG_URL: "http://searxng:8080"
|
||||
|
||||
db:
|
||||
# Postgres 17 with the vector extension Scribe needs.
|
||||
# Postgres 17 with the vector extension Grimoire needs.
|
||||
image: pgvector/pgvector:pg17
|
||||
stop_grace_period: 120s
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
environment:
|
||||
POSTGRES_USER: ${POSTGRES_USER:-scribe}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-scribe}
|
||||
POSTGRES_DB: ${POSTGRES_DB:-scribe}
|
||||
POSTGRES_USER: ${POSTGRES_USER:-grimoire}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-grimoire}
|
||||
POSTGRES_DB: ${POSTGRES_DB:-grimoire}
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
pgdata:
|
||||
app_data:
|
||||
pgdata:
|
||||
|
||||
+14
-14
@@ -1,8 +1,8 @@
|
||||
# API Keys and Scribe MCP
|
||||
# API Keys and Grimoire MCP
|
||||
|
||||
## API Keys
|
||||
|
||||
API keys let external tools access your Scribe data without a browser session. Each key is scoped to a single user — it can only access data that user owns or has been shared with them.
|
||||
API keys let external tools access your Grimoire data without a browser session. Each key is scoped to a single user — it can only access data that user owns or has been shared with them.
|
||||
|
||||
### Scopes
|
||||
|
||||
@@ -22,7 +22,7 @@ Admin-level operations (log access, user management) require a `write`-scoped ke
|
||||
5. Copy the key immediately — it is shown only once (the token is `fmcp_`-prefixed)
|
||||
|
||||
Paste the key into the `Authorization: Bearer <key>` header of your MCP client
|
||||
config (see **Scribe MCP Server** below).
|
||||
config (see **Grimoire MCP Server** below).
|
||||
|
||||
### Revoking a Key
|
||||
|
||||
@@ -30,12 +30,12 @@ Click **Revoke** next to the key in the keys table and confirm. Revoked keys are
|
||||
|
||||
---
|
||||
|
||||
## Scribe MCP Server
|
||||
## Grimoire MCP Server
|
||||
|
||||
Scribe exposes itself as a set of MCP tools that Claude (and other MCP clients)
|
||||
Grimoire exposes itself as a set of MCP tools that Claude (and other MCP clients)
|
||||
can use to read and write your notes, tasks, projects, rulebooks, and more. The
|
||||
server is **built into the app** — it is mounted as a streamable-HTTP endpoint
|
||||
at **`/mcp`** on the running Scribe instance (`src/scribe/mcp/server.py`). There
|
||||
at **`/mcp`** on the running Grimoire instance (`src/grimoire/mcp/server.py`). There
|
||||
is nothing to install: no wheel, no separate package, no CLI. You connect a
|
||||
client straight to the URL with a Bearer token.
|
||||
|
||||
@@ -50,14 +50,14 @@ without a read-shaped name are spelled out here. A `write`-scoped key may call e
|
||||
|
||||
### Claude Code: the plugin (recommended)
|
||||
|
||||
The plugin connects the MCP server and adds the hooks and skills that put Scribe's records in front of Claude as it works. Install it from this repo:
|
||||
The plugin connects the MCP server and adds the hooks and skills that put Grimoire's records in front of Claude as it works. Install it from this repo:
|
||||
|
||||
```
|
||||
/plugin marketplace add https://git.fabledsword.com/bvandeusen/FabledScribe.git
|
||||
/plugin install scribe@scribe-plugin
|
||||
/plugin marketplace add https://git.fabledsword.com/bvandeusen/FabledGrimoire.git
|
||||
/plugin install grimoire@grimoire-plugin
|
||||
```
|
||||
|
||||
It asks for your Scribe base URL, an API key (kept in your OS keychain) and, optionally, a project id. Settings → MCP Access shows the exact command for your instance. [`plugin/README.md`](../plugin/README.md) describes each hook.
|
||||
It asks for your Grimoire base URL, an API key (kept in your OS keychain) and, optionally, a project id. Settings → MCP Access shows the exact command for your instance. [`plugin/README.md`](../plugin/README.md) describes each hook.
|
||||
|
||||
### Claude Code without the plugin
|
||||
|
||||
@@ -67,9 +67,9 @@ your instance's `/mcp` endpoint:
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"scribe": {
|
||||
"grimoire": {
|
||||
"type": "http",
|
||||
"url": "https://your-scribe-instance.example.com/mcp",
|
||||
"url": "https://your-grimoire-instance.example.com/mcp",
|
||||
"headers": {
|
||||
"Authorization": "Bearer fmcp_your-api-key"
|
||||
}
|
||||
@@ -90,7 +90,7 @@ specific project at a dev instance or an admin key.
|
||||
### Available Tools
|
||||
|
||||
There are well over a hundred tools and they change with the app, so the
|
||||
registration in **`src/scribe/mcp/tools/`** is the reference. By family:
|
||||
registration in **`src/grimoire/mcp/tools/`** is the reference. By family:
|
||||
|
||||
| Family | Examples | Purpose |
|
||||
|--------|----------|---------|
|
||||
@@ -111,4 +111,4 @@ registration in **`src/scribe/mcp/tools/`** is the reference. By family:
|
||||
Server-level usage guidance — when to reach for each entity, the
|
||||
recall-before-acting reflex, and the rulebook conventions — is delivered to the
|
||||
client automatically via the MCP server's `instructions` block (defined in
|
||||
`src/scribe/mcp/server.py`).
|
||||
`src/grimoire/mcp/server.py`).
|
||||
|
||||
@@ -219,11 +219,10 @@ endpoint at `/mcp`, not these REST routes.
|
||||
| POST | `/api/notifications/:id/read` | Mark one read |
|
||||
| POST | `/api/notifications/read-all` | Mark all read |
|
||||
|
||||
## Profile and Settings
|
||||
## Settings
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| GET / PUT | `/api/profile` | Read / update the per-user profile |
|
||||
| GET / PUT | `/api/settings` | Read / update key-value settings |
|
||||
| GET | `/api/settings/search` | SearXNG configuration status |
|
||||
| GET / POST | `/api/settings/forge-connections` | Your git forge connections / add one |
|
||||
@@ -252,6 +251,7 @@ These serve the plugin's hooks.
|
||||
| POST | `/api/plugin/rule-opened` | Records that a session opened a rule it was shown |
|
||||
| GET | `/api/plugin/shape-check` | The end-of-turn question about what this turn wrote |
|
||||
| GET | `/api/plugin/claim-session` · `/release-session` | Bind a session to its task claims / release them at session end |
|
||||
| GET | `/api/plugin/write-scope` | Which projects a pending act would change (a directory, or an MCP call's `tool` and `input`), and whether a session in its own project may do it. Filing an issue elsewhere is allowed; every other cross-project write is answered `allowed: false` with the reason the operator is asked |
|
||||
| GET | `/api/plugin/processes` | Stored Processes for skill-stub sync |
|
||||
| GET | `/api/plugin/prior-art` | Write-path hint for the plugin hooks (params: `path`, `code`, `repo`, `shapes`, `exclude_ids`, `exclude_sync_ids`, `exclude_derive`); returns `context`, `note_ids`, `sync_note_ids`, `stamped`, `divergence`, `derive`, `derive_keys` |
|
||||
| GET / POST | `/api/projects/<id>/coverage`, `…/coverage/refresh` | Shape-ledger accounting (`pattern_coverage` line, counts, `derive_groups` — css groups carry `consumers`, `derive_new`, `unused_css`, `divergence`, `recheck`) |
|
||||
@@ -273,12 +273,15 @@ These serve the plugin's hooks.
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| GET | `/api/admin/backup` | Export backup (format v4; `?scope=user` for own data) |
|
||||
| POST | `/api/admin/restore` | Restore from a backup |
|
||||
| GET | `/api/admin/restore/sources` | The backups at the backup destination, newest first |
|
||||
| POST | `/api/admin/restore/preview` | What restoring a backup (`?source=<name>` or an uploaded file) would change; 409 with the reason when it can't apply here |
|
||||
| POST | `/api/admin/restore?confirm=rollback` | Put the whole instance back to that backup, after saving the current state |
|
||||
| GET / DELETE | `/api/admin/users` · `/api/admin/users/:id` | List / delete users |
|
||||
| GET / PUT | `/api/admin/registration` | Get / toggle registration |
|
||||
| GET / PUT | `/api/admin/smtp` · POST `/api/admin/smtp/test` | SMTP config + test email |
|
||||
| GET | `/api/admin/logs` · `/api/admin/logs/stats` | Log entries + category counts |
|
||||
| GET / PUT | `/api/admin/base-url` | Get / set the public base URL |
|
||||
| GET / PUT | `/api/admin/proxy` | Get / set how many reverse proxies stand in front of Grimoire (`{"hops": n}`, or `null` for the environment's count), with what each count would read for this request |
|
||||
| GET / PUT | `/api/admin/db-maintenance` (+ `/health`, POST `/run`) | VACUUM schedule, health, manual run |
|
||||
| POST / GET / DELETE | `/api/admin/invitations` (+ `/:id`) | Create / list / revoke invite links |
|
||||
| GET / PUT | `/api/admin/backups` | Scheduled backups: destination, schedule and recent runs |
|
||||
@@ -292,10 +295,10 @@ These serve the plugin's hooks.
|
||||
|--------|------|-------------|
|
||||
| POST | `/api/webhooks/forge` | Push events from a git forge, verified with `FORGE_WEBHOOK_SECRET` **(public)** |
|
||||
|
||||
## Scribe MCP
|
||||
## Grimoire MCP
|
||||
|
||||
Claude clients connect to the built-in MCP server at `POST /mcp` (streamable HTTP,
|
||||
Bearer auth with an `fmcp_` key), served by `src/scribe/mcp/`. It is not a REST
|
||||
Bearer auth with an `fmcp_` key), served by `src/grimoire/mcp/`. It is not a REST
|
||||
surface — it exposes the same data as typed tools (`create_note`, `create_task`,
|
||||
`start_planning`, `search`, `enter_project`, …) with
|
||||
server-level usage guidance delivered in the MCP `instructions` block. See
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Architecture
|
||||
|
||||
Scribe is one app container and one PostgreSQL container. There is no language model in the stack: Claude works with Scribe from outside, through the MCP endpoint and the Claude Code plugin, and people use the web UI.
|
||||
Grimoire is one app container and one PostgreSQL container. There is no language model in the stack: Claude works with Grimoire from outside, through the MCP endpoint and the Claude Code plugin, and people use the web UI.
|
||||
|
||||
## Stack
|
||||
|
||||
@@ -24,10 +24,10 @@ Scribe is one app container and one PostgreSQL container. There is no language m
|
||||
```
|
||||
|
||||
- **The web UI** is a Vue single-page app, built into the image and served by Quart. Every unknown path that isn't under `/api/` falls back to `index.html`; `/api/*` always answers JSON.
|
||||
- **The REST API** (`src/scribe/routes/`) backs the UI, one blueprint per resource.
|
||||
- **The MCP server** (`src/scribe/mcp/`) is how Claude reads and writes records. Each module in `mcp/tools/` registers its tools, and the server's instructions and each tool's docstring are the contract an agent reads. Requests authenticate with an API key as a Bearer token.
|
||||
- **The REST API** (`src/grimoire/routes/`) backs the UI, one blueprint per resource.
|
||||
- **The MCP server** (`src/grimoire/mcp/`) is how Claude reads and writes records. Each module in `mcp/tools/` registers its tools, and the server's instructions and each tool's docstring are the contract an agent reads. Requests authenticate with an API key as a Bearer token.
|
||||
- **The plugin** (`plugin/`) runs in the user's Claude Code. Its hooks call `/api/plugin/*` to put the right context in front of Claude at the right moment: the project at session start, matching rules and prior art before a prompt or a tool call, the rules mounted on a moment of work, and a check on the reply that ends a turn. Its skills describe the working practices. See [API Keys & MCP](api-keys-and-mcp.md) to install it.
|
||||
- **Services** (`src/scribe/services/`) hold the logic both doors share. Routes and MCP tools stay thin: parse the request, check access, call a service, return a plain dict.
|
||||
- **Services** (`src/grimoire/services/`) hold the logic both doors share. Routes and MCP tools stay thin: parse the request, check access, call a service, return a plain dict.
|
||||
|
||||
## Data model
|
||||
|
||||
@@ -69,25 +69,25 @@ Every database wait on startup has a deadline. Migrations wait for the database
|
||||
|
||||
## Authentication
|
||||
|
||||
- **Sessions**: `HttpOnly`, `SameSite=Lax` cookies, `Secure` when `SECURE_COOKIES=true`. Changing a password bumps the user's session version and signs out their other sessions.
|
||||
- **Sessions**: `HttpOnly`, `SameSite=Lax` cookies, `Secure` on a request the trusted proxy reports as HTTPS, which also gets HSTS (`grimoire/proxy.py`). Changing a password bumps the user's session version and signs out their other sessions.
|
||||
- **API keys**: `Authorization: Bearer fmcp_...`, stored hashed. A `read` key may only call read endpoints and read MCP tools.
|
||||
- **OIDC** single sign-on with PKCE, optionally replacing local logins ([SSO / OAuth](sso-oauth.md)).
|
||||
- **Rate limits** on register, login and forgot-password, per client IP (`TRUST_PROXY_HEADERS` reads the IP from the proxy's headers).
|
||||
- **Rate limits** on register, login and forgot-password, per client IP, read from `X-Forwarded-For` by `TRUSTED_PROXY_HOPS`.
|
||||
|
||||
Credentials Scribe stores for outside services (forge tokens, the backup destination's S3 secret) are sealed under a key derived from `SECRET_KEY`.
|
||||
Credentials Grimoire stores for outside services (forge tokens, the backup destination's S3 secret) are sealed under a key derived from `SECRET_KEY`, which the instance makes on first start and keeps in `/data/secret_key` unless one is set.
|
||||
|
||||
## Repository layout
|
||||
|
||||
```
|
||||
├── alembic/versions/ database migrations
|
||||
├── src/scribe/
|
||||
├── src/grimoire/
|
||||
│ ├── app.py app factory, blueprints, startup and shutdown
|
||||
│ ├── config.py environment configuration
|
||||
│ ├── models/ SQLAlchemy models
|
||||
│ ├── routes/ REST blueprints
|
||||
│ ├── mcp/ MCP server and its tool modules
|
||||
│ ├── services/ shared logic
|
||||
│ └── restore.py `python -m scribe.restore` for a fresh instance
|
||||
│ └── restore.py `python -m grimoire.restore`: the restore, from the command line
|
||||
├── frontend/src/ views, components, stores, composables, api client
|
||||
├── plugin/ Claude Code hooks, skills and commands
|
||||
├── scripts/ CI checks and the plugin version minter
|
||||
|
||||
+74
-39
@@ -1,7 +1,7 @@
|
||||
# Backups
|
||||
|
||||
Scribe can back itself up every day to a **folder** or an **S3-compatible
|
||||
bucket**. Each run adds one file; Scribe never deletes an old one. Keeping and
|
||||
Grimoire can back itself up every day to a **folder** or an **S3-compatible
|
||||
bucket**. Each run adds one file; Grimoire never deletes an old one. Keeping and
|
||||
thinning the history is the destination's job.
|
||||
|
||||
Set it up under **Settings → Data → Scheduled backups** (admins only).
|
||||
@@ -11,24 +11,28 @@ Set it up under **Settings → Data → Scheduled backups** (admins only).
|
||||
One file per run, named for the moment it was taken in UTC:
|
||||
|
||||
```
|
||||
scribe-20261015T020000Z.tar
|
||||
grimoire-20261015T020000Z.tar
|
||||
```
|
||||
|
||||
Backups written while the app was called Scribe are named `scribe-…tar`.
|
||||
They are counted with the rest and restore the same way.
|
||||
|
||||
It is a plain, **uncompressed** tar:
|
||||
|
||||
- `manifest.json` comes first. It holds the backup format version, the Scribe
|
||||
- `manifest.json` comes first. It holds the backup format version, the Grimoire
|
||||
version, when the backup was taken, and how many rows each table has.
|
||||
- Then there is one `<table>.jsonl` per table, one row per line.
|
||||
|
||||
It holds everything the admin **Full Backup** download holds, laid out
|
||||
differently. It does **not** hold:
|
||||
|
||||
- **Search indexes.** They are rebuilt from the records when Scribe starts.
|
||||
- **Search indexes.** They are rebuilt from the records when Grimoire starts.
|
||||
- **Sharing:** groups, project shares and note shares.
|
||||
- **Credentials:** API keys and forge connections. After a restore, issue new
|
||||
API keys and add forge connections again. (Forge tokens are also sealed under
|
||||
`SECRET_KEY` in the database itself, so a copy of the database doesn't carry
|
||||
them in readable form either.)
|
||||
them in readable form either. The key lives in `/data/secret_key`, outside
|
||||
the database.)
|
||||
- **Logs and notifications.**
|
||||
- **The backup destination's own S3 secret.**
|
||||
|
||||
@@ -61,16 +65,16 @@ file again.
|
||||
|
||||
### A folder
|
||||
|
||||
A path **inside the Scribe container**, normally a share mounted there:
|
||||
A path **inside the Grimoire container**, normally a share mounted there:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
scribe:
|
||||
grimoire:
|
||||
volumes:
|
||||
- /nfs/backup/scribe:/backups
|
||||
- /nfs/backup/grimoire:/backups
|
||||
```
|
||||
|
||||
Scribe writes each file under a hidden name (`.scribe-….tar.part`) and renames
|
||||
Grimoire writes each file under a hidden name (`.grimoire-….tar.part`) and renames
|
||||
it once it is complete. A program watching the folder never sees half a
|
||||
backup.
|
||||
|
||||
@@ -79,14 +83,17 @@ backup.
|
||||
You need four things:
|
||||
|
||||
- **Address:** the service itself, like `https://backup.example.com`. Don't
|
||||
include the bucket; Scribe addresses buckets by path.
|
||||
include the bucket; Grimoire addresses buckets by path.
|
||||
- **Bucket.**
|
||||
- **Access key.**
|
||||
- **Secret key.**
|
||||
|
||||
Scribe uploads and lists; it never deletes. A key that may **only add
|
||||
objects** is therefore enough, and it is the safer key to hand out: a stolen
|
||||
one cannot wipe the history.
|
||||
Grimoire uploads and lists, and reads a backup back when you restore from the
|
||||
bucket; it never deletes. A key that may **add and read objects but not
|
||||
delete them** is therefore enough, and it is the safer key to hand out: a
|
||||
stolen one cannot wipe the history. A key that may only add objects still
|
||||
takes backups; restore then works by uploading a file or from the command
|
||||
line.
|
||||
|
||||
The secret is stored encrypted, under a key derived from `SECRET_KEY`. It is
|
||||
never shown again, and never included in a backup. If an instance's
|
||||
@@ -115,46 +122,74 @@ page asks for the secret again.
|
||||
[Fabled Archives](https://git.fabledsword.com/bvandeusen/fabledarchives)
|
||||
keeps service backups as "backup targets". Either destination works with it.
|
||||
|
||||
1. On Archives' **Backups** page, make a target for Scribe, named `scribe`
|
||||
1. On Archives' **Backups** page, make a target for Grimoire, named `grimoire`
|
||||
for example.
|
||||
2. Choose how Scribe's files arrive:
|
||||
- **S3**: turn on the S3 way in for the target and make an **append-only**
|
||||
S3 key. In Scribe, choose **An S3 bucket**:
|
||||
2. Choose how Grimoire's files arrive:
|
||||
- **S3**: turn on the S3 way in for the target and make an S3 key that
|
||||
may add and read objects but not delete them (an **append-only** key
|
||||
takes backups too, but then Grimoire can't restore from the bucket). In
|
||||
Grimoire, choose **An S3 bucket**:
|
||||
- address: Archives' backup address (`http://<archives>:8081`);
|
||||
- bucket: the target's name;
|
||||
- access key and secret key: the key you just made.
|
||||
- **A folder**: mount the share Archives watches into Scribe as
|
||||
`/backups/scribe`, and point the target at that folder. In Scribe, choose
|
||||
**A folder** with `/backups/scribe`.
|
||||
- **A folder**: mount the share Archives watches into Grimoire as
|
||||
`/backups/grimoire`, and point the target at that folder. In Grimoire, choose
|
||||
**A folder** with `/backups/grimoire`.
|
||||
3. Set the target's **Retention** to **Archives**. Archives then keeps the
|
||||
newest backups and thins the rest by day, week and month. Scribe only adds.
|
||||
newest backups and thins the rest by day, week and month. Grimoire only adds.
|
||||
4. Give the target an expected cadence of **daily**, so a missed backup turns
|
||||
it late in Archives as well as overdue in Scribe.
|
||||
it late in Archives as well as overdue in Grimoire.
|
||||
5. Add an off-site **restic** replication job for the target. The uncompressed
|
||||
layout above is what lets that job send only what changed each day.
|
||||
|
||||
## Restoring
|
||||
|
||||
### Into a fresh instance
|
||||
A restore puts the **whole instance** back to the moment a backup was taken,
|
||||
for every user:
|
||||
|
||||
Start Scribe on an empty database, copy the backup file into the container,
|
||||
and run:
|
||||
- records edited since go back to how they were;
|
||||
- records deleted since come back;
|
||||
- anything made since is removed.
|
||||
|
||||
```sh
|
||||
docker exec -it scribe python -m scribe.restore /backups/scribe-20261015T020000Z.tar
|
||||
```
|
||||
It matches records by their ids, so a live instance only restores a backup **of
|
||||
its own**. Each instance has an id, and every backup records the one it came
|
||||
from. A backup from another instance, or one made before backups recorded it,
|
||||
only goes into a new, empty instance, which takes it as it is.
|
||||
|
||||
- The command refuses a database that already has an account. A restore
|
||||
re-creates everything with new ids, so on a live instance it would add a
|
||||
second copy instead of replacing anything.
|
||||
- Nothing limits the file's size.
|
||||
- It also reads the JSON from the admin **Full Backup** download.
|
||||
Before changing anything, the restore saves the current state as one more
|
||||
backup. It goes to the backup destination when scheduled backups are on, and
|
||||
otherwise to `/data/pre-restore`. The result says which file it is, so a
|
||||
restore can itself be undone.
|
||||
|
||||
Afterwards, **restart Scribe** so the search indexes are rebuilt. Then issue
|
||||
new API keys, add forge connections again, and set up any sharing.
|
||||
What a backup doesn't carry stays as it is wherever its owner survives the
|
||||
restore: API keys, forge connections, sharing, logs, and the destination's S3
|
||||
secret. The trash isn't in backups, so anything in the trash when the backup
|
||||
was taken is gone afterwards. Everyone is signed out, and search is rebuilt.
|
||||
|
||||
### From the web
|
||||
|
||||
**Settings → Data → Restore from Backup** takes the `.tar` as well as the
|
||||
JSON download. The upload is held to the server's request-size limit. For a
|
||||
bigger file, use the command above.
|
||||
**Settings → Data → Restore from Backup** lists the backups at the backup
|
||||
destination. You can pick one of those, or upload a `.tar` or the JSON
|
||||
**Full Backup** download (uploads are held to the server's request-size
|
||||
limit). The restore then shows what would change: when the backup was taken,
|
||||
and how many accounts, projects, plans, notes and rules there are now and will
|
||||
be afterwards. Type `roll back` to run it. Then sign in again.
|
||||
|
||||
To read backups back from an S3 bucket, the key needs read access as well as
|
||||
write. Grimoire never deletes from the bucket.
|
||||
|
||||
### From the command line
|
||||
|
||||
For a file with no size limit, or a new instance with no backup destination
|
||||
yet, copy the file into the container and run:
|
||||
|
||||
```sh
|
||||
docker exec -it grimoire python -m grimoire.restore /backups/grimoire-20261015T020000Z.tar
|
||||
```
|
||||
|
||||
It prints what would change and stops. Add `--yes` to restore. It reads the
|
||||
JSON download too. Afterwards, **restart Grimoire** so the search indexes are
|
||||
rebuilt.
|
||||
|
||||
After restoring into a new instance, issue new API keys, add forge connections
|
||||
again, and set up any sharing: none of them travel in a backup.
|
||||
|
||||
+32
-23
@@ -1,8 +1,8 @@
|
||||
# Configuration
|
||||
|
||||
Scribe reads its configuration from environment variables. Most settings people change day to day (registration, SMTP, retrieval thresholds, backups, forge connections) live in **Settings** in the web UI instead.
|
||||
Grimoire reads its configuration from environment variables. Most settings people change day to day (registration, SMTP, retrieval thresholds, backups, forge connections) live in **Settings** in the web UI instead.
|
||||
|
||||
Any variable marked *secret* can also be read from a file: set `<NAME>_FILE` to the path, for example a Docker secret at `/run/secrets/scribe_key`.
|
||||
Any variable marked *secret* can also be read from a file: set `<NAME>_FILE` to the path, for example a Docker secret at `/run/secrets/grimoire_key`.
|
||||
|
||||
## Environment variables
|
||||
|
||||
@@ -10,11 +10,11 @@ Any variable marked *secret* can also be read from a file: set `<NAME>_FILE` to
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `DATABASE_URL` *(secret)* | `postgresql+asyncpg://scribe:scribe@localhost:5432/scribe` | PostgreSQL connection string, using the `asyncpg` driver |
|
||||
| `SECRET_KEY` *(secret)* | `dev-secret-change-me` | Signs sessions and seals stored credentials. Set it before first use and keep it (see below) |
|
||||
| `DATABASE_URL` *(secret)* | `postgresql+asyncpg://grimoire:grimoire@localhost:5432/grimoire` | PostgreSQL connection string, using the `asyncpg` driver |
|
||||
| `SECRET_KEY` *(secret)* | kept in `DATA_DIR` | Signs sessions and seals stored credentials. Unset, the instance makes its own on first start and keeps it in `DATA_DIR/secret_key`. Set it only to manage the key yourself; it wins over the kept one (see below) |
|
||||
| `DATA_DIR` | `/data` | Where the instance keeps its own `SECRET_KEY`. Mount a volume here |
|
||||
| `BASE_URL` | `http://localhost:5000` | Public URL, used in email links and OIDC redirects |
|
||||
| `SECURE_COOKIES` | `false` | `true` when Scribe is served over HTTPS |
|
||||
| `TRUST_PROXY_HEADERS` | `false` | `true` behind a reverse proxy, to read the client IP from `X-Forwarded-For` / `X-Real-IP` |
|
||||
| `TRUSTED_PROXY_HOPS` | `1` | The starting count of reverse proxies in front of Grimoire. **Settings → Config → Reverse proxy** is where the count is checked and set, and a count saved there wins over this. The client address and the scheme are read that many entries in from the right of `X-Forwarded-For` and `X-Forwarded-Proto`; `0` ignores both headers. See [Behind a reverse proxy](#behind-a-reverse-proxy) |
|
||||
| `LOG_LEVEL` | `INFO` | `DEBUG`, `INFO`, `WARNING` or `ERROR` |
|
||||
| `LOG_RETENTION_DAYS` | `90` | Days of app logs to keep |
|
||||
| `FASTEMBED_CACHE_DIR` | `/data/fastembed-cache` | Where the embedding model is stored after its first download |
|
||||
@@ -42,7 +42,7 @@ Admins can set SMTP in **Settings**; these are the defaults until they do.
|
||||
| `SMTP_USERNAME` | — |
|
||||
| `SMTP_PASSWORD` *(secret)* | — |
|
||||
| `SMTP_FROM_ADDRESS` | — |
|
||||
| `SMTP_FROM_NAME` | `Fabled Scribe` |
|
||||
| `SMTP_FROM_NAME` | `Fabled Grimoire` |
|
||||
| `SMTP_USE_TLS` | `true` |
|
||||
|
||||
### Integrations
|
||||
@@ -56,50 +56,55 @@ Admins can set SMTP in **Settings**; these are the defaults until they do.
|
||||
|
||||
## Running it
|
||||
|
||||
### Quick start
|
||||
### Docker Compose
|
||||
|
||||
[`docker-compose.quickstart.yml`](../docker-compose.quickstart.yml) runs the published image with its own database:
|
||||
[`docker-compose.yml`](../docker-compose.yml) runs the published image with its own database, and needs nothing set:
|
||||
|
||||
```bash
|
||||
SECRET_KEY=$(python3 -c 'import secrets; print(secrets.token_hex(32))') \
|
||||
docker compose -f docker-compose.quickstart.yml up -d
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
Open `http://localhost:5000`. The first account registered becomes the admin; after that, registration stays closed until an admin opens it in Settings or sends an invite.
|
||||
|
||||
Update with `docker compose pull && docker compose up -d`. The key and the data stay in their volumes.
|
||||
|
||||
### From source
|
||||
|
||||
```bash
|
||||
cp .env.example .env # then set SECRET_KEY
|
||||
docker compose up --build
|
||||
docker compose -f docker-compose.dev.yml up --build
|
||||
```
|
||||
|
||||
### Docker Swarm
|
||||
|
||||
[`docker-compose.prod.yml`](../docker-compose.prod.yml) is a stack file: the app and database on a private overlay network, with `SECURE_COOKIES` and `TRUST_PROXY_HEADERS` on. Provide `DB_PASSWORD` and `SECRET_KEY` in the environment:
|
||||
[`docker-compose.prod.yml`](../docker-compose.prod.yml) is a stack file: the app and database on a private overlay network, trusting one proxy (`TRUSTED_PROXY_HOPS: "1"`). A Swarm can move the app to another node, away from a node-local `/data` volume, so the stack sets its own key: provide `DB_PASSWORD` and `SECRET_KEY` in the environment (or mount a Docker secret and set `SECRET_KEY_FILE`):
|
||||
|
||||
```bash
|
||||
DB_PASSWORD=... SECRET_KEY=... docker stack deploy -c docker-compose.prod.yml scribe
|
||||
DB_PASSWORD=... SECRET_KEY=... docker stack deploy -c docker-compose.prod.yml grimoire
|
||||
```
|
||||
|
||||
The database user, database and volume default to `grimoire`. An install created under the name Scribe keeps its data by setting `POSTGRES_USER`, `POSTGRES_DB` and `PGDATA_VOLUME` to the names it was created with.
|
||||
|
||||
The shipped files leave out healthchecks and restart policies on purpose. The app waits for its database on startup, and the orchestrator's defaults restart it if it ever exits.
|
||||
|
||||
### Volumes
|
||||
|
||||
- **Database** (`pgdata`): everything Scribe stores.
|
||||
- **App data** (`/data`): the embedding model cache and diagnostics. Losing it costs a re-download, nothing else.
|
||||
- **Database** (`pgdata`): everything Grimoire stores.
|
||||
- **App data** (`/data`): the instance's own `SECRET_KEY`, the embedding model cache and diagnostics. Losing it costs the key: Settings then asks for saved forge tokens and the backup secret again, and the model downloads again.
|
||||
|
||||
Back up the database with **Settings → Data → Scheduled backups** ([Backups](backups.md)).
|
||||
Back up the database with **Settings → Data → Scheduled backups** ([Backups](backups.md)). A database backup doesn't carry the key; to move sealed credentials to a new instance, copy `/data/secret_key` with it.
|
||||
|
||||
## Behind a reverse proxy
|
||||
|
||||
Scribe doesn't terminate TLS. Put it behind Nginx, Traefik or Caddy, forward to port 5000, and keep port 5000 off the internet. Set `SECURE_COOKIES=true`, `TRUST_PROXY_HEADERS=true` and `BASE_URL`.
|
||||
Grimoire doesn't terminate TLS. Put it behind Nginx, Traefik or Caddy, forward to port 5000, and keep port 5000 off the internet. Set `BASE_URL`, then set the proxy count to the number of proxies a request passes through: one proxy is `1`, the default; Cloudflare's proxied DNS in front of your own proxy is `2`.
|
||||
|
||||
**Settings → Config → Reverse proxy** sets the count and checks it against your own request. It shows the address Grimoire reads you as, and what each count from 0 to 3 would read. Pick the count that shows your own address. When a request comes through Cloudflare, the section names the count that reads the address Cloudflare reports, with a button that sets it. `TRUSTED_PROXY_HOPS` in the environment is only the starting value: a count saved in Settings wins, and **Use the environment's** goes back to it.
|
||||
|
||||
A count that is too low makes every visitor read as the proxy, so they share one sign-in rate limit and a few bad sign-ins block everyone. The "too many attempts" message says so and points here. The block on sign-ins lifts after a minute; an admin who is already signed in can fix the count at once.
|
||||
|
||||
```nginx
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:5000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
@@ -109,12 +114,16 @@ location / {
|
||||
}
|
||||
```
|
||||
|
||||
With `TRUST_PROXY_HEADERS=true`, make sure the proxy replaces any `X-Forwarded-For` the client sends.
|
||||
Each proxy appends its entry to `X-Forwarded-For` and `X-Forwarded-Proto`, so Grimoire reads the entry that many in from the right: the one your own proxy wrote. Whatever a client puts to the left of it is ignored. A header with fewer entries than that (a LAN road with one proxy beside a Cloudflare road with two) is read at its leftmost entry, the one the first proxy wrote.
|
||||
|
||||
When that entry says `https`, the session cookie is marked `Secure` and the response carries `Strict-Transport-Security: max-age=31536000`. Over plain HTTP neither is sent, and Grimoire never redirects to HTTPS, so a LAN install keeps working.
|
||||
|
||||
A hop count higher than the real number of proxies lets a client choose its own address, so count them. With `0`, the headers are ignored and the socket peer is the client: use it when clients reach port 5000 directly.
|
||||
|
||||
## Security checklist
|
||||
|
||||
- **Set a strong `SECRET_KEY`, and keep it.** Forge tokens and the backup destination's S3 secret are sealed under a key derived from it, so a copy of the database alone can't read them. If the key changes (say, a restore onto an instance with a different key), Settings marks those credentials and asks for them again; nothing else breaks. The app logs a warning at startup while the default key is in use, and refuses to start with it when `SECURE_COOKIES=true`.
|
||||
- **`SECURE_COOKIES=true`** whenever Scribe is served over HTTPS.
|
||||
- **Keep the key.** Forge tokens and the backup destination's S3 secret are sealed under a key derived from `SECRET_KEY`, so a copy of the database alone can't read them. Unset, the instance makes a random key and keeps it in `/data/secret_key`, readable by its owner only; keep that volume. If the key changes (say, a restore onto an instance with a different key), Settings marks those credentials and asks for them again; nothing else breaks. The app warns at startup when `SECRET_KEY` is a public placeholder or `/data` is not a mounted volume, and refuses to start with a placeholder key (`dev-secret-change-me`, `change-me-in-production`).
|
||||
- **The proxy count** (Settings → Config → Reverse proxy) matches the number of proxies in front of Grimoire, so rate limits and the audit log see the real client and the cookie is `Secure` over HTTPS. The section shows whether it does.
|
||||
- **Registration** is open only for the first account. Invite people, or open it briefly from Settings.
|
||||
- **Signing everyone else out**: changing a password ends that user's other sessions; Settings → Account has a button for it too.
|
||||
- **API keys** are per user, shown once, and can be read-only. Revoke any you no longer use under Settings → MCP Access.
|
||||
|
||||
+13
-11
@@ -53,7 +53,7 @@ Parchment is intentionally not pure white — it's slightly warm to feel like ag
|
||||
| Bronze | `#8B7355` | Secondary action buttons (Cancel-but-not-destructive, alternate paths) |
|
||||
| Pewter | `#3F4651` | Tertiary/ghost buttons |
|
||||
|
||||
**Critical rule:** Action button colors are universal across all apps. A Save button in Scribe and a Save button in Minstrel look identical. Per-app accents do not appear on buttons.
|
||||
**Critical rule:** Action button colors are universal across all apps. A Save button in Grimoire and a Save button in Minstrel look identical. Per-app accents do not appear on buttons.
|
||||
|
||||
### Semantic colors
|
||||
|
||||
@@ -71,7 +71,7 @@ Parchment is intentionally not pure white — it's slightly warm to feel like ag
|
||||
|
||||
| App | Hex | Mood |
|
||||
|---|---|---|
|
||||
| Fabled Scribe | `#5B4A8A` | Dusty violet — ink, manuscripts |
|
||||
| Fabled Grimoire | `#5B4A8A` | Dusty violet — ink, manuscripts |
|
||||
| Minstrel | `#4A6B5C` | Forest teal — music, performance |
|
||||
| Fabled Forge | `#8B5A2B` | Forge bronze — creation, craft |
|
||||
| Roundtable | `#4A5D7E` | Slate blue — stewardship, infrastructure |
|
||||
@@ -80,7 +80,7 @@ Parchment is intentionally not pure white — it's slightly warm to feel like ag
|
||||
**Accent usage rules:**
|
||||
- The accent appears on the app's wordmark and icon.
|
||||
- The accent indicates active/current state in nav (the selected page, the active tab).
|
||||
- The accent is the cursor color and text-selection color in long-form surfaces (Scribe notes, Forge story drafts).
|
||||
- The accent is the cursor color and text-selection color in long-form surfaces (Grimoire notes, Forge story drafts).
|
||||
- The accent does NOT appear on primary or secondary action buttons.
|
||||
- The accent does NOT appear in body text or chrome.
|
||||
- One accent per app. Don't mix accents within a single app.
|
||||
@@ -101,6 +101,8 @@ All text-on-surface combinations meet WCAG AA at minimum. Parchment on Obsidian
|
||||
| Inter | Body, UI, labels | Google Fonts |
|
||||
| JetBrains Mono | Code, terminal output, monospaced data | Google Fonts |
|
||||
|
||||
All three are served by the app itself, from `frontend/src/assets/fonts/`, with every subset Google Fonts publishes for them. A page never asks a font host for anything, so an instance with no internet still draws in its own type. They are under the SIL Open Font License; the licences sit beside the files.
|
||||
|
||||
### Why this pairing
|
||||
|
||||
Fraunces is a contemporary serif with personality — it has the warmth and authority of a book serif without feeling like costume. It signals "this is considered" without signaling "this is a fantasy product." Inter is the workhorse — neutral, ubiquitous, designed for screens, doesn't compete with the serif. JetBrains Mono is the natural choice for any developer-adjacent product and supports ligatures.
|
||||
@@ -124,7 +126,7 @@ Fraunces is a contemporary serif with personality — it has the warmth and auth
|
||||
- **Sentence case everywhere.** Never Title Case for headings, never ALL CAPS except for the Tiny micro-label style.
|
||||
- **Two weights only:** 400 regular and 500 medium. Never 600 or 700 — they read heavy in dark mode.
|
||||
- **Fraunces only at 18px and above.** Below that it loses too much detail and feels fragile. For h3 and below, use Inter.
|
||||
- **Line height** 1.5 for body, 1.3 for headings, 1.7 for long-form reading surfaces (Scribe notes, Forge drafts).
|
||||
- **Line height** 1.5 for body, 1.3 for headings, 1.7 for long-form reading surfaces (Grimoire notes, Forge drafts).
|
||||
- **Letter-spacing** at default for everything except the Tiny micro-label, which gets `0.08em` letter-spacing and uppercase styling.
|
||||
|
||||
---
|
||||
@@ -173,7 +175,7 @@ Padding: `8px 16px` for default, `6px 12px` for compact, `10px 20px` for promine
|
||||
|
||||
Used for tags, hashtags, code spans, status badges. Background is the accent color at ~15% opacity, text is the accent at full strength. Border-radius 4px, padding `2px 8px`, Inter 11px/500.
|
||||
|
||||
In Scribe specifically, hashtags and tags use the dusty violet accent. In Minstrel, they'd use forest teal. The pattern is shared; the color follows the app.
|
||||
In Grimoire specifically, hashtags and tags use the dusty violet accent. In Minstrel, they'd use forest teal. The pattern is shared; the color follows the app.
|
||||
|
||||
### Cards
|
||||
|
||||
@@ -272,9 +274,9 @@ Use **Lucide** (https://lucide.dev) as the base icon set — it matches this sty
|
||||
|
||||
## Per-app application
|
||||
|
||||
### Fabled Scribe (#5B4A8A — dusty violet)
|
||||
### Fabled Grimoire (#5B4A8A — dusty violet)
|
||||
|
||||
A second-brain notes and task management tool. The accent appears in: the wordmark, hashtags and tag pills, the active nav item, text selection color, and the cursor in the editor. Notes are presented on Iron-surfaced cards with generous reading line-height. The hashtag system uses Scribe's accent for visual continuity.
|
||||
A second-brain notes and task management tool. The accent appears in: the wordmark, hashtags and tag pills, the active nav item, text selection color, and the cursor in the editor. Notes are presented on Iron-surfaced cards with generous reading line-height. The hashtag system uses Grimoire's accent for visual continuity.
|
||||
|
||||
### Minstrel (#4A6B5C — forest teal)
|
||||
|
||||
@@ -332,7 +334,7 @@ Home server management. The accent appears on: the wordmark, healthy/online stat
|
||||
}
|
||||
|
||||
/* Per-app accent — set ONE of these on the root for each app */
|
||||
[data-app="scribe"] { --fs-accent: #5B4A8A; }
|
||||
[data-app="grimoire"] { --fs-accent: #5B4A8A; }
|
||||
[data-app="minstrel"] { --fs-accent: #4A6B5C; }
|
||||
[data-app="forge"] { --fs-accent: #8B5A2B; }
|
||||
[data-app="roundtable"]{ --fs-accent: #4A5D7E; }
|
||||
@@ -354,11 +356,11 @@ If using Tailwind, extend the theme with these tokens rather than relying on def
|
||||
|
||||
---
|
||||
|
||||
# Scribe
|
||||
# Grimoire
|
||||
|
||||
The design system Scribe renders with is stored in Scribe itself: **Scribe**, which inherits **FabledSword**, edited at `/design-systems`. `frontend/src/assets/theme.css` is generated from it, and CI rejects a `var()` the sheet doesn't define. Where this page and the stored system differ, the stored system is right. Scribe applies the baseline above with these choices:
|
||||
The design system Grimoire renders with is stored in Grimoire itself: **Grimoire**, which inherits **FabledSword**, edited at `/design-systems`. `frontend/src/assets/theme.css` is generated from it, and CI rejects a `var()` the sheet doesn't define. Where this page and the stored system differ, the stored system is right. Grimoire applies the baseline above with these choices:
|
||||
|
||||
- **Accent footprint.** Dusty violet marks Scribe's own moments: the wordmark, active navigation, the editor's cursor and selection, tags and wikilinks, in-progress status, focus rings, and the call to action that starts something new (a first note, say). Operating the software uses the action colours: Moss to save or confirm, Bronze for alternatives, Oxblood with an icon to delete, Pewter ghost for tertiary actions.
|
||||
- **Accent footprint.** Dusty violet marks Grimoire's own moments: the wordmark, active navigation, the editor's cursor and selection, tags and wikilinks, in-progress status, focus rings, and the call to action that starts something new (a first note, say). Operating the software uses the action colours: Moss to save or confirm, Bronze for alternatives, Oxblood with an icon to delete, Pewter ghost for tertiary actions.
|
||||
- **Light and dark.** Both are supported. Light mode is warm parchment (`#F5F1E8` page, `#FBF8F0` cards, `#14171A` text) rather than white, with a cool wash on code blocks so syntax colours stay readable. The accent, action and semantic colours are the same in both.
|
||||
- **Status and priority.** Status: todo Pewter, in progress the accent, done Moss, cancelled Ash, paused Warning. Priority runs cool to warm: low Info, medium Warning, high Error. Overdue uses Error. Each has a low-alpha `-bg` companion.
|
||||
- **Type.** Inter for UI and body, Fraunces at 18px and up (display, H1, H2), JetBrains Mono for code. Two weights only, 400 and 500. Line height 1.5 for body, 1.3 for headings, 1.7 on reading surfaces such as a note.
|
||||
|
||||
+7
-7
@@ -3,9 +3,9 @@
|
||||
## Running it
|
||||
|
||||
```bash
|
||||
docker compose up --build # app + PostgreSQL at http://localhost:5000
|
||||
docker compose up --build app # rebuild after a change
|
||||
docker compose down -v # stop and wipe the database
|
||||
docker compose -f docker-compose.dev.yml up --build # app + PostgreSQL at http://localhost:5000
|
||||
docker compose -f docker-compose.dev.yml up --build app # rebuild after a change
|
||||
docker compose -f docker-compose.dev.yml down -v # stop and wipe the database
|
||||
```
|
||||
|
||||
The image builds the frontend, so a frontend change needs a rebuild too. For quicker frontend work, run Vite against the running app; it proxies `/api` to port 5000:
|
||||
@@ -32,7 +32,7 @@ Unit tests run without a database. Tests marked `integration` need a real Postgr
|
||||
The container runs `alembic upgrade head` on every start, after waiting for the database to accept connections.
|
||||
|
||||
```bash
|
||||
docker compose exec app alembic revision -m "short_description"
|
||||
docker compose -f docker-compose.dev.yml exec app alembic revision -m "short_description"
|
||||
```
|
||||
|
||||
- Number revisions sequentially (`0123_...`). Revision ids must fit in 32 characters, the width of `alembic_version.version_num`.
|
||||
@@ -45,7 +45,7 @@ docker compose exec app alembic revision -m "short_description"
|
||||
|
||||
`plugin/` ships straight from git: installs fetch it through `.claude-plugin/marketplace.json`, and there is no build step. After changing anything under `plugin/` or `.claude-plugin/`, run `make mint-plugin` before committing. Claude Code only refreshes an installed plugin when its version changes, and CI fails a plugin change that didn't bump it.
|
||||
|
||||
The hooks must run on a bare machine: POSIX shell, awk, sed and the usual coreutils, plus `git` and `curl`. They fail open, so a slow or unreachable Scribe never blocks Claude Code.
|
||||
The hooks must run on a bare machine: POSIX shell, awk, sed and the usual coreutils, plus `git` and `curl`. They fail open, so a slow or unreachable Grimoire never blocks Claude Code.
|
||||
|
||||
## CI and releases
|
||||
|
||||
@@ -57,7 +57,7 @@ CI runs on Gitea Actions (workflow in `.forgejo/workflows/ci.yml`) in the shared
|
||||
| `main` | `:latest`, `:<sha>` |
|
||||
| a `v*` tag | `:latest`, `:<version>`, `:<sha>` |
|
||||
|
||||
`:<sha>` never moves, so it is the rollback point. Images publish to `git.fabledsword.com/bvandeusen/fabledscribe`, with a registry layer cache at `:cache`. The build needs the `REGISTRY_USER` and `REGISTRY_TOKEN` (a token with `write:packages`) repository secrets.
|
||||
`:<sha>` never moves, so it is the rollback point. Images publish to `git.fabledsword.com/bvandeusen/fabledgrimoire`, with a registry layer cache at `:cache`. The build needs the `REGISTRY_USER` and `REGISTRY_TOKEN` (a token with `write:packages`) repository secrets.
|
||||
|
||||
Work happens on `dev`. A release merges `dev` into `main` with a plain merge commit; a dated `vYYYY.MM.DD.HHMM` tag, matching the version the image reports, can mark it.
|
||||
|
||||
@@ -65,7 +65,7 @@ Work happens on `dev`. A release merges `dev` into `main` with a plain merge com
|
||||
|
||||
### Backend
|
||||
|
||||
- Services open their own session: `async with async_session() as session:` (from `scribe.models`).
|
||||
- Services open their own session: `async with async_session() as session:` (from `grimoire.models`).
|
||||
- Routes and MCP tools stay thin; the logic lives in `services/`, shared by both.
|
||||
- Access checks go through `services/access.py`, never inline ownership checks.
|
||||
- Every wait on something outside the process has a deadline.
|
||||
|
||||
+11
-11
@@ -1,6 +1,6 @@
|
||||
# Features
|
||||
|
||||
Scribe is a self-hosted system of record for the work you do with Claude Code. Claude records what it learned, decided and built, and that record reaches the next session, so a solution is recalled rather than reinvented. There is no language model inside Scribe: Claude reaches it through the MCP endpoint and the Claude Code plugin, and the web UI is where people read and edit the same records.
|
||||
Grimoire is a self-hosted system of record for the work you do with Claude Code. Claude records what it learned, decided and built, and that record reaches the next session, so a solution is recalled rather than reinvented. There is no language model inside Grimoire: Claude reaches it through the MCP endpoint and the Claude Code plugin, and the web UI is where people read and edit the same records.
|
||||
|
||||
## Notes
|
||||
|
||||
@@ -40,8 +40,8 @@ A task is a note with a status (`todo`, `in_progress`, `done`, `cancelled`), plu
|
||||
## Snippets and the shape ledger
|
||||
|
||||
- **Snippets** are recorded prior art: a reusable piece of code with when to reach for it, its signature and its canonical location in a repo. Claude searches them before building and is shown matching ones before it writes a file.
|
||||
- **Forge integration**: with a git forge connected, Scribe checks a snippet's cached body against its source and marks it current or diverged. Push webhooks keep that fresh.
|
||||
- **The shape ledger**: Scribe extracts the code shapes from a bound repo and tracks how each is accounted for: an instance of a snippet, a deliberate variant with its reason, or a one-off. Coverage shows what is still unclassified.
|
||||
- **Forge integration**: with a git forge connected, Grimoire checks a snippet's cached body against its source and marks it current or diverged. Push webhooks keep that fresh.
|
||||
- **The shape ledger**: Grimoire extracts the code shapes from a bound repo and tracks how each is accounted for: an instance of a snippet, a deliberate variant with its reason, or a one-off. Coverage shows what is still unclassified.
|
||||
|
||||
## Family canon
|
||||
|
||||
@@ -49,11 +49,11 @@ Projects on the same platform (Android apps, Go services, ...) share good ideas.
|
||||
|
||||
## Design systems
|
||||
|
||||
Tokens (colour, type, space, motion, ...) and written guidance, with inheritance: a project's design system can extend a house style and record where it departs. Scribe generates a stylesheet from it, checks code snippets against it, and gives Claude the values to use before it writes UI. `/design-systems` edits them.
|
||||
Tokens (colour, type, space, motion, ...) and written guidance, with inheritance: a project's design system can extend a house style and record where it departs. Grimoire generates a stylesheet from it, checks code snippets against it, and gives Claude the values to use before it writes UI. `/design-systems` edits them.
|
||||
|
||||
## Stored processes
|
||||
|
||||
Saved procedures, such as a drift audit or a DRY pass. The plugin syncs each one into a Claude Code skill (`/scribe:sync`), which fetches the live procedure when it is invoked.
|
||||
Saved procedures, such as a drift audit or a DRY pass. The plugin syncs each one into a Claude Code skill (`/grimoire:sync`), which fetches the live procedure when it is invoked.
|
||||
|
||||
## Search and context injection
|
||||
|
||||
@@ -65,8 +65,9 @@ Saved procedures, such as a drift audit or a DRY pass. The plugin syncs each one
|
||||
|
||||
The whole store is reachable through the MCP endpoint at `/mcp`, authenticated with an API key. The Claude Code plugin in this repo adds:
|
||||
|
||||
- hooks for session start, each prompt, before and after tool calls, the end of a turn, compaction and session end, all failing open if Scribe is unreachable;
|
||||
- skills for the working practices: using Scribe, writing plans, systematic debugging, verification, brainstorming, reporting back, reusing code, shape accounting and family canon;
|
||||
- hooks for session start, each prompt, before and after tool calls, the end of a turn, compaction and session end, all failing open if Grimoire is unreachable;
|
||||
- a project fence: a session belongs to the project its starting directory is bound to. Toward any other project it can read anything and file an issue. Editing that project's files, committing or pushing in its repo, or changing its records first asks you, in every permission mode;
|
||||
- skills for the working practices: using Grimoire, writing plans, systematic debugging, verification, brainstorming, reporting back, reusing code, shape accounting and family canon;
|
||||
- your stored processes as skills.
|
||||
|
||||
See [API Keys & MCP](api-keys-and-mcp.md).
|
||||
@@ -85,8 +86,8 @@ Share a project, note or task with users or groups, as viewer, editor or admin.
|
||||
## Export and backup
|
||||
|
||||
- **Personal export**: your notes and tasks as a Markdown ZIP (with YAML frontmatter) or JSON.
|
||||
- **Admin backup and restore**, with ids remapped so a backup restores onto another instance.
|
||||
- **Scheduled backups**: a daily, dated, uncompressed tar to a folder or an S3-compatible bucket, laid out so a deduplicating tool such as restic stores only what changed. `python -m scribe.restore` brings a fresh instance back from one. See [Backups](backups.md).
|
||||
- **Admin backup and restore**: a restore rolls the whole instance back to one of its own backups, after saving the current state, or brings a new instance back from any backup.
|
||||
- **Scheduled backups**: a daily, dated, uncompressed tar to a folder or an S3-compatible bucket, laid out so a deduplicating tool such as restic stores only what changed. Restoring is in Settings, or `python -m grimoire.restore`. See [Backups](backups.md).
|
||||
|
||||
## Sign-in
|
||||
|
||||
@@ -98,12 +99,11 @@ Email and password, plus optional OIDC single sign-on (Authentik, Keycloak, ...)
|
||||
|-----|----------|
|
||||
| General | Timezone, trash retention, retrieval thresholds, moments, reply shapes |
|
||||
| Account | Email, password, active sessions |
|
||||
| Profile | About you, response preferences, interests, work schedule |
|
||||
| Notifications | Email notifications |
|
||||
| Integrations | SearXNG, git forge connections |
|
||||
| Data | Export, backup and restore, scheduled backups (admin) |
|
||||
| MCP Access | API keys and the command to connect Claude |
|
||||
| Config (admin) | What's running, application URL, plugin marketplace, database maintenance, SMTP, forge webhook |
|
||||
| Config (admin) | What's running, reverse proxy count (checked against your own request), application URL, plugin marketplace, database maintenance, SMTP, forge webhook |
|
||||
| Shared areas (admin) | The canonical areas systems map to |
|
||||
| Platforms (admin) | The platform catalogue family canon is grouped by |
|
||||
| Users (admin) | Registration, invites, users |
|
||||
|
||||
+5
-5
@@ -1,6 +1,6 @@
|
||||
# OAuth / OIDC SSO Setup
|
||||
|
||||
Scribe supports single sign-on via any OpenID Connect provider.
|
||||
Grimoire supports single sign-on via any OpenID Connect provider.
|
||||
This guide covers Authentik, but the same pattern works with Keycloak, Authelia, Zitadel, etc.
|
||||
|
||||
## 1. Create the provider in Authentik
|
||||
@@ -8,7 +8,7 @@ This guide covers Authentik, but the same pattern works with Keycloak, Authelia,
|
||||
1. Log in to the Authentik admin UI.
|
||||
2. Go to **Applications → Providers → Create → OAuth2/OpenID Provider**.
|
||||
3. Fill in:
|
||||
- **Name:** `Scribe` (or whatever you like)
|
||||
- **Name:** `Grimoire` (or whatever you like)
|
||||
- **Authorization flow:** your default authorization flow
|
||||
- **Client type:** `Confidential`
|
||||
- **Redirect URIs:** `https://your-fabled-domain/api/auth/oauth/callback`
|
||||
@@ -20,7 +20,7 @@ This guide covers Authentik, but the same pattern works with Keycloak, Authelia,
|
||||
https://auth.example.com/application/o/fabled-assistant/
|
||||
```
|
||||
|
||||
## 2. Configure Scribe
|
||||
## 2. Configure Grimoire
|
||||
|
||||
Add the following environment variables to the `app` service in `docker-compose.yml`:
|
||||
|
||||
@@ -64,7 +64,7 @@ docker compose up --build -d
|
||||
|
||||
## 4. Account Linking
|
||||
|
||||
When a user logs in via OAuth for the first time, Scribe checks in this order:
|
||||
When a user logs in via OAuth for the first time, Grimoire checks in this order:
|
||||
|
||||
1. **Existing OAuth sub** — returns that user immediately.
|
||||
2. **Matching email** — if a local account already exists with the same email, the OAuth identity is linked to it automatically. The user retains all their notes and tasks.
|
||||
@@ -92,4 +92,4 @@ The backend will reject any `POST /api/auth/login` or `POST /api/auth/register`
|
||||
| Zitadel | `https://your-instance.zitadel.cloud` |
|
||||
| Google | `https://accounts.google.com` |
|
||||
|
||||
The OIDC discovery endpoint (`<issuer>/.well-known/openid-configuration`) must be publicly reachable from the Scribe container (server-to-server call at login time).
|
||||
The OIDC discovery endpoint (`<issuer>/.well-known/openid-configuration`) must be publicly reachable from the Grimoire container (server-to-server call at login time).
|
||||
|
||||
+4
-3
@@ -4,13 +4,14 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
|
||||
<link rel="manifest" href="/manifest.json">
|
||||
<meta name="theme-color" content="#6c63ff">
|
||||
<meta name="theme-color" content="#14171A">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
<meta name="apple-mobile-web-app-title" content="Scribe">
|
||||
<title>Fabled Scribe</title>
|
||||
<meta name="apple-mobile-web-app-title" content="Grimoire">
|
||||
<title>Fabled Grimoire</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
Generated
+3
-2
@@ -1,12 +1,13 @@
|
||||
{
|
||||
"name": "scribe-frontend",
|
||||
"name": "grimoire-frontend",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "scribe-frontend",
|
||||
"name": "grimoire-frontend",
|
||||
"version": "0.1.0",
|
||||
"license": "AGPL-3.0-only",
|
||||
"dependencies": {
|
||||
"@fullcalendar/core": "^6.1.20",
|
||||
"@fullcalendar/daygrid": "^6.1.20",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "scribe-frontend",
|
||||
"name": "grimoire-frontend",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0-only",
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 4.4 KiB |
+16
-31
@@ -1,33 +1,18 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32">
|
||||
<style>
|
||||
.book { fill: #6366f1; stroke: #4f46e5; }
|
||||
.spine { stroke: #4f46e5; }
|
||||
.lines { stroke: #a5b4fc; }
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.book { fill: #e2e8f0; stroke: #cbd5e0; }
|
||||
.spine { stroke: #cbd5e0; }
|
||||
.lines { stroke: #4a5568; }
|
||||
}
|
||||
</style>
|
||||
<!-- Book body -->
|
||||
<path class="book" d="M4 7 C4 7 8 5 16 6 C24 5 28 7 28 7 L28 26 C28 26 24 24 16 25 C8 24 4 26 4 26 Z" stroke-width="0.5"/>
|
||||
<!-- Book spine -->
|
||||
<line class="spine" x1="16" y1="6" x2="16" y2="25" stroke-width="0.8"/>
|
||||
<!-- Left page lines -->
|
||||
<line class="lines" x1="7" y1="11" x2="14" y2="10.5" stroke-width="0.6" stroke-linecap="round"/>
|
||||
<line class="lines" x1="7" y1="14" x2="14" y2="13.5" stroke-width="0.6" stroke-linecap="round"/>
|
||||
<line class="lines" x1="7" y1="17" x2="14" y2="16.5" stroke-width="0.6" stroke-linecap="round"/>
|
||||
<line class="lines" x1="7" y1="20" x2="12" y2="19.5" stroke-width="0.6" stroke-linecap="round"/>
|
||||
<!-- Right page lines -->
|
||||
<line class="lines" x1="18" y1="10.5" x2="25" y2="11" stroke-width="0.6" stroke-linecap="round"/>
|
||||
<line class="lines" x1="18" y1="13.5" x2="25" y2="14" stroke-width="0.6" stroke-linecap="round"/>
|
||||
<line class="lines" x1="18" y1="16.5" x2="25" y2="17" stroke-width="0.6" stroke-linecap="round"/>
|
||||
<!-- Magic spark -->
|
||||
<g transform="translate(24, 5)">
|
||||
<path d="M0 -4 L1 -1 L4 0 L1 1 L0 4 L-1 1 L-4 0 L-1 -1 Z" fill="#f6ad55"/>
|
||||
<path d="M0 -2.5 L0.6 -0.6 L2.5 0 L0.6 0.6 L0 2.5 L-0.6 0.6 L-2.5 0 L-0.6 -0.6 Z" fill="#fbd38d"/>
|
||||
</g>
|
||||
<!-- Secondary sparkles -->
|
||||
<circle cx="20" cy="3" r="0.7" fill="#f6ad55" opacity="0.7"/>
|
||||
<circle cx="27" cy="8" r="0.5" fill="#fbd38d" opacity="0.6"/>
|
||||
<!-- page block, seen at the fore-edge -->
|
||||
<path d="M8.5 4.5 H25.5 Q27 4.5 27 6 V27 Q27 28.5 25.5 28.5 H8.5 Z" fill="#E9DFC6"/>
|
||||
<path d="M25 7 V26.5" stroke="#C9BB97" stroke-width="0.6"/>
|
||||
<!-- cover -->
|
||||
<path d="M5 4 Q5 3 6 3 H24 Q25.5 3 25.5 4.5 V26.5 Q25.5 28 24 28 H6 Q5 28 5 27 Z" fill="#5B4A8A"/>
|
||||
<!-- spine band -->
|
||||
<rect x="5" y="3" width="3.2" height="25" rx="0.6" fill="#47396E"/>
|
||||
<!-- corner fittings -->
|
||||
<path d="M21.5 3 H24 Q25.5 3 25.5 4.5 V7 Z" fill="#D4A94E"/>
|
||||
<path d="M21.5 28 H24 Q25.5 28 25.5 26.5 V24 Z" fill="#D4A94E"/>
|
||||
<!-- clasp across the fore-edge -->
|
||||
<rect x="22.5" y="14" width="5.5" height="3.6" rx="0.8" fill="#D4A94E"/>
|
||||
<circle cx="26.4" cy="15.8" r="0.7" fill="#8A6A22"/>
|
||||
<!-- sigil: a four-pointed star in a ring -->
|
||||
<circle cx="15.5" cy="15.5" r="5.2" fill="none" stroke="#D4A94E" stroke-width="0.9"/>
|
||||
<path d="M15.5 11.4 L16.5 14.5 L19.6 15.5 L16.5 16.5 L15.5 19.6 L14.5 16.5 L11.4 15.5 L14.5 14.5 Z" fill="#F1D48A"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.0 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 5.1 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
@@ -1,17 +1,27 @@
|
||||
{
|
||||
"name": "Fabled Scribe",
|
||||
"short_name": "Scribe",
|
||||
"description": "Your self-hosted second brain with AI assistance",
|
||||
"name": "Fabled Grimoire",
|
||||
"short_name": "Grimoire",
|
||||
"description": "A self-hosted work system-of-record for software projects, built to be driven by Claude Code.",
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"background_color": "#1a1a2e",
|
||||
"theme_color": "#6c63ff",
|
||||
"background_color": "#14171A",
|
||||
"theme_color": "#14171A",
|
||||
"orientation": "portrait-primary",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/favicon.ico",
|
||||
"src": "/favicon.svg",
|
||||
"sizes": "any",
|
||||
"type": "image/x-icon"
|
||||
"type": "image/svg+xml"
|
||||
},
|
||||
{
|
||||
"src": "/icon-192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/icon-512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { apiGet, apiPost, apiPut } from "@/api/client";
|
||||
import { apiGet, apiPost, apiPostRaw, apiPut } from "@/api/client";
|
||||
|
||||
/**
|
||||
* Scheduled backups (milestone 523) — `/api/admin/backups`. One dated,
|
||||
* Scheduled backups — `/api/admin/backups`. One dated,
|
||||
* uncompressed tar per run, delivered to a folder or an S3 bucket.
|
||||
*/
|
||||
export interface BackupConfig {
|
||||
@@ -71,3 +71,72 @@ export function checkBackupDestination(
|
||||
export function runBackupNow(): Promise<{ status: string }> {
|
||||
return apiPost<{ status: string }>("/api/admin/backups/run", {});
|
||||
}
|
||||
|
||||
|
||||
// ── Restore: put the whole instance back to a full backup ─────────────────
|
||||
|
||||
export interface StoredBackup {
|
||||
name: string;
|
||||
size_bytes: number;
|
||||
}
|
||||
|
||||
export interface RestoreCount {
|
||||
kind: string;
|
||||
now: number;
|
||||
after: number;
|
||||
}
|
||||
|
||||
export interface RestorePreview {
|
||||
/** "fresh": an empty instance takes the backup. "rollback": this instance
|
||||
* goes back to its own earlier backup. */
|
||||
mode: "fresh" | "rollback";
|
||||
exported_at: string | null;
|
||||
counts: RestoreCount[];
|
||||
}
|
||||
|
||||
export interface RestoreResult extends RestorePreview {
|
||||
status: "ok";
|
||||
restored: Record<string, number>;
|
||||
safety_backup: { file: string; size_bytes: number; where: string; note?: string };
|
||||
}
|
||||
|
||||
/** A backup at the destination, by name, or a file picked from this computer. */
|
||||
export type RestoreFrom = { source: string } | { file: File };
|
||||
|
||||
// Reading, checking and writing the whole store is slow by design; bounded,
|
||||
// but in minutes.
|
||||
const RESTORE_TIMEOUT_MS = 10 * 60 * 1000;
|
||||
|
||||
export function listRestoreSources(): Promise<{ backups: StoredBackup[]; error?: string }> {
|
||||
return apiGet("/api/admin/restore/sources", { timeoutMs: 60_000 });
|
||||
}
|
||||
|
||||
async function restoreCall<T>(path: string, from: RestoreFrom, query: Record<string, string> = {}): Promise<T> {
|
||||
const params = new URLSearchParams(query);
|
||||
if ("source" in from) {
|
||||
params.set("source", from.source);
|
||||
return apiPostRaw<T>(`${path}?${params}`, null, null, { timeoutMs: RESTORE_TIMEOUT_MS });
|
||||
}
|
||||
const qs = params.toString() ? `?${params}` : "";
|
||||
const isTar = from.file.name.toLowerCase().endsWith(".tar");
|
||||
// The JSON download is parsed first, so a damaged file fails here, before
|
||||
// the upload.
|
||||
const body = isTar ? from.file : JSON.stringify(JSON.parse(await from.file.text()));
|
||||
return apiPostRaw<T>(`${path}${qs}`, body, isTar ? "application/x-tar" : "application/json",
|
||||
{ timeoutMs: RESTORE_TIMEOUT_MS });
|
||||
}
|
||||
|
||||
export function previewRestore(from: RestoreFrom): Promise<RestorePreview> {
|
||||
return restoreCall<RestorePreview>("/api/admin/restore/preview", from);
|
||||
}
|
||||
|
||||
export function restoreBackup(from: RestoreFrom): Promise<RestoreResult> {
|
||||
return restoreCall<RestoreResult>("/api/admin/restore", from, { confirm: "rollback" });
|
||||
}
|
||||
|
||||
/** When a backup named `grimoire-20261015T020000Z.tar` was taken. */
|
||||
export function backupTakenAt(name: string): Date | null {
|
||||
const m = /(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})Z/.exec(name);
|
||||
if (!m) return null;
|
||||
return new Date(Date.UTC(+m[1], +m[2] - 1, +m[3], +m[4], +m[5], +m[6]));
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Canonical systems — the GLOBAL area vocabulary every project's Systems can
|
||||
* map onto (milestone 307).
|
||||
* map onto.
|
||||
*
|
||||
* The mapping is an ASSOCIATION, never a rename: a project's System keeps the
|
||||
* name the project gave it, and `canonical_id` only records which shared area
|
||||
|
||||
+15
-25
@@ -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) {
|
||||
@@ -55,21 +55,20 @@ export function apiErrorMessage(e: unknown, fallback: string): string {
|
||||
/**
|
||||
* How long an ordinary JSON call may wait before it is declared failed.
|
||||
*
|
||||
* Rule 156: a wait with no deadline is a bug. `fetch`'s own default is to wait
|
||||
* A wait with no deadline is a bug. `fetch`'s own default is to wait
|
||||
* as long as the browser will, which is not a deadline — it is the absence of
|
||||
* one, and it renders as a spinner that never resolves. There is no state a
|
||||
* 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.
|
||||
*/
|
||||
const DEFAULT_TIMEOUT_MS = 30_000;
|
||||
|
||||
/** HTTP 408. Not a status any Scribe route returns, so it unambiguously means
|
||||
/** HTTP 408. Not a status any Grimoire route returns, so it unambiguously means
|
||||
* "the client gave up" rather than anything the server said. */
|
||||
const CLIENT_TIMEOUT_STATUS = 408;
|
||||
|
||||
@@ -118,7 +117,7 @@ export interface RequestOpts {
|
||||
* The one place a request is actually made — every verb below goes through
|
||||
* here, so the deadline cannot be forgotten by adding a sixth.
|
||||
*
|
||||
* EXPIRY SURFACES AS AN `ApiError`, which is rule 156's second half: the
|
||||
* EXPIRY SURFACES AS AN `ApiError`, the deadline's second half: the
|
||||
* failure has to arrive in the shape the caller already handles. A bare
|
||||
* `DOMException: TimeoutError` would reach `apiErrorMessage(e, fallback)` as
|
||||
* an object with no `body`, so every catch site in the app would report its
|
||||
@@ -157,6 +156,15 @@ export function apiPost<T>(path: string, body: unknown, opts?: RequestOpts): Pro
|
||||
return request<T>(path, { method: "POST", headers: JSON_HEADERS, body: JSON.stringify(body) }, opts);
|
||||
}
|
||||
|
||||
/** A POST whose body is sent as it is (a file, or text already serialised),
|
||||
* under the same deadline and error handling as every other call. */
|
||||
export function apiPostRaw<T>(
|
||||
path: string, body: BodyInit | null, contentType: string | null, opts?: RequestOpts,
|
||||
): Promise<T> {
|
||||
const headers = contentType ? { "Content-Type": contentType } : undefined;
|
||||
return request<T>(path, { method: "POST", headers, body }, opts);
|
||||
}
|
||||
|
||||
export function apiPut<T>(path: string, body: unknown, opts?: RequestOpts): Promise<T> {
|
||||
return request<T>(path, { method: "PUT", headers: JSON_HEADERS, body: JSON.stringify(body) }, opts);
|
||||
}
|
||||
@@ -501,24 +509,6 @@ export const createApiKey = (name: string, scope: 'read' | 'write') =>
|
||||
|
||||
export const revokeApiKey = (id: number) => apiDelete(`/api/api-keys/${id}`)
|
||||
|
||||
// ── User Profile ─────────────────────────────────────────────────────────────
|
||||
|
||||
export interface UserProfile {
|
||||
display_name: string
|
||||
job_title: string
|
||||
industry: string
|
||||
expertise_level: 'novice' | 'intermediate' | 'expert'
|
||||
response_style: 'concise' | 'balanced' | 'detailed'
|
||||
tone: 'casual' | 'professional' | 'technical'
|
||||
interests: string[]
|
||||
work_schedule: { days?: string[]; start?: string; end?: string }
|
||||
}
|
||||
|
||||
export const getProfile = () => apiGet<UserProfile>('/api/profile')
|
||||
export const updateProfile = (data: Partial<UserProfile>) =>
|
||||
apiPut<UserProfile>('/api/profile', data)
|
||||
|
||||
|
||||
// ── Note Versions (pinning) ──────────────────────────────────────────────────
|
||||
|
||||
import type { NoteVersion } from '../types/task'
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Family canon (milestone 463): ideas every project on a platform shares, and
|
||||
* Family canon: ideas every project on a platform shares, and
|
||||
* the log of every decision about them.
|
||||
*
|
||||
* The agent promotes by written criteria — no approval step. This door is for
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
/** Project inception (milestone 297): what a project was decided to inherit. */
|
||||
/** Project inception: what a project was decided to inherit. */
|
||||
import { apiGet, apiPost } from "@/api/client";
|
||||
import type { ProjectPlatform } from "@/api/platforms";
|
||||
|
||||
export interface InceptionChoices {
|
||||
design_system_id: number | null;
|
||||
seed_systems: boolean;
|
||||
/** Platform slugs (milestone 463). null = not answered; a list is the
|
||||
/** Platform slugs. null = not answered; a list is the
|
||||
* whole answer — members left out of it are recorded as "not this".
|
||||
* Absent on records decided before the question existed. */
|
||||
platforms?: string[] | null;
|
||||
|
||||
@@ -72,7 +72,7 @@ export interface Lesson {
|
||||
tags: string[];
|
||||
note_type: string;
|
||||
/** Where it was LEARNED. Kept as a fact, but not a limit on where it can be
|
||||
* found: a lesson is retrievable from every project (milestone 385 step 3). */
|
||||
* found: a lesson is retrievable from every project. */
|
||||
project_id: number | null;
|
||||
permission?: string;
|
||||
created_at: string | null;
|
||||
@@ -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;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { apiGet, apiPost, apiDelete } from "@/api/client";
|
||||
|
||||
/**
|
||||
* The moments of work rules mount on (milestone 458), and the actions that
|
||||
* The moments of work rules mount on, and the actions that
|
||||
* reach each one on this install — `GET /api/retrieval/moments`, the payload
|
||||
* the `list_moments` MCP tool returns, plus the counts the Settings view shows
|
||||
* beside each moment.
|
||||
@@ -44,6 +44,14 @@ export interface RemovedDefault {
|
||||
created_at: string | null;
|
||||
}
|
||||
|
||||
export interface MomentRuleUsage {
|
||||
rule_id: number;
|
||||
/** Null for a rule deleted since it was delivered. */
|
||||
title: string | null;
|
||||
delivered: number;
|
||||
opened: boolean;
|
||||
}
|
||||
|
||||
export interface MomentUsage {
|
||||
/** How many times a mounted rule arrived at this moment. */
|
||||
delivered: number;
|
||||
@@ -53,6 +61,8 @@ export interface MomentUsage {
|
||||
* the door, not the line that prompted it. */
|
||||
opened: number;
|
||||
last_delivered_at: string | null;
|
||||
/** Each rule delivered here, most delivered first. */
|
||||
by_rule: MomentRuleUsage[];
|
||||
}
|
||||
|
||||
export interface MomentsPayload {
|
||||
@@ -102,9 +112,8 @@ export function unmapAction(change: MappingChange): Promise<void> {
|
||||
|
||||
/**
|
||||
* Proposals about a rule's moments, waiting on a person. A `mount` proposal
|
||||
* (milestone 458 step 7) comes from a pass that read the rule, or from the
|
||||
* rule being opened just after the moment fired. An `unmount` proposal (step
|
||||
* 7b) comes from sessions reporting that the mount arrived where it did not
|
||||
* comes from a pass that read the rule, or from the rule being opened just
|
||||
* after the moment fired. An `unmount` proposal comes from sessions reporting that the mount arrived where it did not
|
||||
* apply. `GET /api/retrieval/moments/proposals`, the payload the
|
||||
* `rule_moment_proposals` MCP tool returns.
|
||||
*/
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user