diff --git a/.githooks/commit-msg b/.githooks/commit-msg index 7930d3b..ed64736 100755 --- a/.githooks/commit-msg +++ b/.githooks/commit-msg @@ -1,5 +1,5 @@ #!/usr/bin/env sh -# Strips assistant attribution trailers from every commit message. +# Strips attribution trailers from every commit message. # # House rule: commits carry the owner's name and nothing else. Tooling that # appends its own attribution is stripped here rather than caught in review, @@ -30,7 +30,7 @@ rm -f "$tmp" # message is true, and nothing else did either. # # **It warns and never blocks**, deliberately. A message legitimately names paths it does not -# touch — the file a fix refers to, a path being explained, a command being quoted — so a +# touch (the file a fix refers to, a path being explained, a command being quoted), so a # blocking version would be wrong far more often than right. This exists to put the claim and # the diff in front of the author at the moment of writing, not to arbitrate. # @@ -66,12 +66,12 @@ for tok in $(tr -c 'A-Za-z0-9_./-' ' ' < "$msg_file"); do done if [ -n "$missing" ]; then - echo "commit-msg: WARNING — the message names paths that exist nowhere in the tree:" >&2 + echo "commit-msg: WARNING: the message names paths that exist nowhere in the tree:" >&2 for m in $missing; do echo " $m" >&2; done fi if [ -n "$unstaged" ]; then - echo "commit-msg: NOTE — the message names paths this commit does not touch:" >&2 + echo "commit-msg: NOTE: the message names paths this commit does not touch:" >&2 for m in $unstaged; do echo " $m" >&2; done echo " Fine when the message refers to a file; wrong when it claims to have" >&2 echo " changed one. This is the noisy tier and it is meant to be read, not obeyed." >&2 diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..ab48c93 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,29 @@ +# SPDX-FileCopyrightText: Sudo Apt Holdings LLC +# SPDX-License-Identifier: Apache-2.0 +# +# Dependabot opens a pull request when a dependency has a security advisory or a newer +# release. A pull request is not an upgrade: VERSIONS.md is the pin list, and a version +# lands only after it is proposed in a slice's NOTES.md and approved (CLAUDE.md section 5). +# The value here is the alert arriving as a diff with the gate run against it. +version: 2 +updates: + - package-ecosystem: mix + directory: / + schedule: + interval: weekly + open-pull-requests-limit: 5 + commit-message: + prefix: "chore(deps)" + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + commit-message: + prefix: "chore(ci)" + - package-ecosystem: cargo + directory: /src-tauri + schedule: + interval: weekly + open-pull-requests-limit: 3 + commit-message: + prefix: "chore(deps)" diff --git a/.github/workflows/gate.yml b/.github/workflows/gate.yml index 2b594fd..41a7a41 100644 --- a/.github/workflows/gate.yml +++ b/.github/workflows/gate.yml @@ -13,16 +13,23 @@ jobs: MIX_ENV: test TRINITY_DB: sqlite steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: fetch-depth: 0 # plan_check rule 8 reads the whole history - - - uses: erlef/setup-beam@v1 + # On a pull request the default checkout is a merge commit GitHub makes on the fly, + # authored by nobody and signed off by nobody. plan_check rule 8 refused it on the + # first pull request this repository ever had (run 35477492177). The branch head is + # what was written and signed, so that is what the gate reads. The ruleset's strict + # policy requires the branch to be current with main before it can merge, so the + # head is also what main will contain. + ref: ${{ github.event.pull_request.head.sha || github.sha }} + + - uses: erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124 # v1.24.1 with: version-file: .tool-versions version-type: strict - - uses: actions/cache@v4 + - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: path: | deps diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml index e25a038..f8276cf 100644 --- a/.github/workflows/package.yml +++ b/.github/workflows/package.yml @@ -10,13 +10,26 @@ name: package # Corrected 2026-09-06, before this workflow had ever run. The push trigger was -# `branches: [main]`, so the one branch whose evidence depends on it — the slice branch — was +# `branches: [main]`, so the one branch whose evidence depends on it, the slice branch, was # the one branch it ignored, and pushing slice 001 produced no package run at all. Slice # branches are where a packaging change is proven; main is where it has already been proven. +# +# Narrowed 2026-09-19. Between 2026-09-05 and 2026-09-07 this matrix ran 31 times on every +# push, 1 600 runner-minutes, most of them Windows and macOS, for a packaging path that +# slice 001 had already proven. It now runs when a slice or release tag is pushed, on a +# change to something packaging actually depends on, or by hand. A slice that touches +# packaging asks for the run with `workflow_dispatch` and cites the run id in its proof. on: push: - branches: [main, 'slice/**'] - pull_request: + tags: ['slice/**', 'v*'] + paths: + - mix.exs + - mix.lock + - config/** + - src-tauri/** + - rust-toolchain.toml + - .tool-versions + - .github/workflows/package.yml workflow_dispatch: jobs: @@ -41,9 +54,9 @@ jobs: artifact: desktop_windows_x86_64.exe steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - - uses: erlef/setup-beam@v1 + - uses: erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124 # v1.24.1 with: version-file: .tool-versions version-type: strict @@ -56,12 +69,12 @@ jobs: shell: bash run: echo "version=$(awk '$1=="zig"{print $2}' .tool-versions)" >> "$GITHUB_OUTPUT" - - uses: mlugg/setup-zig@v2 + - uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2.2.1 with: version: ${{ steps.zig.outputs.version }} # The Rust version is not written here either: rustup reads rust-toolchain.toml, which - # is the pin (see NOTES.md D1 — asdf has no rust plugin and ignores a rust line). + # is the pin (see NOTES.md D1: asdf has no rust plugin and ignores a rust line). - name: Rust toolchain from rust-toolchain.toml shell: bash run: rustup show active-toolchain @@ -103,7 +116,7 @@ jobs: # * daisyui (https://github.com/saadeghi/daisyui.git - v5.5.20) # lock mismatch: the dependency is out of date # - # A second `mix deps.get` under MIX_ENV=prod did NOT fix it — it reported "All + # A second `mix deps.get` under MIX_ENV=prod did NOT fix it: it reported "All # dependencies have been fetched" and the release refused anyway, so the checkout on # disk is what Mix disagrees with, not the environment. Both offenders are git # `sparse` deps carrying `app: false, compile: false`; they exist for the asset build @@ -139,7 +152,7 @@ jobs: # Item 2 of the G4 decision. `src-tauri/` exists from slice 001's ex_tauri.install, so # the shell is buildable in CI. This compiles the Rust window; it does not run it, and - # no job here claims a window opened — a runner has no desktop session. + # no job here claims a window opened: a runner has no desktop session. - name: Build the Tauri shell shell: bash run: cargo build --manifest-path src-tauri/Cargo.toml --locked @@ -157,14 +170,14 @@ jobs: # --no-halt is what makes the exit mean anything. Burrito launches the release as # `-s elixir start_cli`, and the Elixir CLI halts when its command list is empty, so # without --no-halt the binary exits 0 on its own and `--smoke` proves nothing. - - name: Smoke — boots, serves, exits by itself, leaves nothing behind + - name: Smoke test: boots, serves, exits by itself, leaves nothing behind if: runner.os != 'Windows' shell: bash run: | set -euo pipefail # Match OUR processes, not the machine's. The first version of this step diffed the # whole `ps -eo pid,ppid,comm` table and failed on macOS because the runner's own - # daemons churn between the two samples — mdworker_shared exiting, CloudTelemetry + # daemons churn between the two samples: mdworker_shared exiting, CloudTelemetry # starting, and the `ps` process itself differing. The artifact had launched, served # and exited cleanly; the assertion was wrong, not the binary. "No process of ours # remains" is the claim AC7 actually makes. @@ -179,7 +192,7 @@ jobs: echo "--- ours, after ----"; cat ps-after.txt diff ps-before.txt ps-after.txt - - name: Smoke — Windows + - name: Smoke test on Windows if: runner.os == 'Windows' shell: pwsh run: | @@ -222,7 +235,7 @@ jobs: done test -n "${PORT:-}" # Bounded. The first version of this loop had no limit and the Windows runner sat in - # it until the run was cancelled by hand — a step that hangs reports nothing and + # it until the run was cancelled by hand: a step that hangs reports nothing and # fails nothing, which is the same shape as a workflow that never fires. ok=0 for _ in $(seq 1 600); do @@ -238,11 +251,11 @@ jobs: code=$(curl -sS -o /dev/null -w '%{http_code}' "http://127.0.0.1:$PORT/") echo "HTTP $code on port $PORT" echo "COLD_START_MS=$elapsed" | tee -a "$GITHUB_ENV" - echo "### ${{ matrix.name }} — cold start to first HTTP 200: **${elapsed} ms**" >> "$GITHUB_STEP_SUMMARY" + echo "### ${{ matrix.name }}: cold start to first HTTP 200: **${elapsed} ms**" >> "$GITHUB_STEP_SUMMARY" test "$code" = "200" # Stopping it, and Windows needs its own verb. `kill` from Git-bash does not stop a # native Windows process, so the backgrounded .exe outlived the step and the job hung - # long past the bounded loops above — run 34078281292, cancelled by hand. `taskkill + # long past the bounded loops above: run 34078281292, cancelled by hand. `taskkill # /T` takes the wrapper and its BEAM together, which on Windows is also the only # thing that clears finding F1's orphan. if [ "${{ runner.os }}" = "Windows" ]; then @@ -251,19 +264,19 @@ jobs: else CHILD=$(pgrep -P "$PID" 2>/dev/null || true) kill "$PID" || true - # The wrapper does not forward termination to the BEAM it launched — finding F1, + # The wrapper does not forward termination to the BEAM it launched: finding F1, # re-measured at G4 as an orphan still serving 200 past 7.6 s. Kill the child # explicitly or the runner leaves it behind. [ -n "$CHILD" ] && kill "$CHILD" 2>/dev/null || true fi - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: ${{ matrix.artifact }} path: burrito_out/${{ matrix.artifact }} if-no-files-found: error - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 if: always() with: name: launch-log-${{ matrix.target }} @@ -272,14 +285,14 @@ jobs: serve.log if-no-files-found: warn - # Line 10. Each job states, in its own summary, which of the two things it did — and + # Line 10. Each job states, in its own summary, which of the two things it did, and # every job here did the second one. - name: State what this job did not prove if: always() shell: bash run: | { - echo "## ${{ matrix.name }} — what this run does and does not establish" + echo "## ${{ matrix.name }}: what this run does and does not establish" echo echo "**Established:** the artifact builds on ${{ matrix.os }}; it launches and" echo "reaches serving; under \`--no-halt --smoke\` it exits by itself and the" @@ -291,7 +304,7 @@ jobs: echo echo "**Not established, and not claimed:** the shell was **built, never run.**" echo "No native window was opened. This job smoked the **sidecar alone, with no" - echo "display** — it did not run the shell under \`xvfb-run\` and it did not use a" + echo "display**: it did not run the shell under \`xvfb-run\` and it did not use a" echo "desktop session, because this runner has none. No screenshot exists here;" echo "the only screenshot of a real window in this project is the owner's, on" echo "Linux, at slices/001-packaging-spike/proof/. First **paint** was not" diff --git a/.gitignore b/.gitignore index 94e20ac..d123a64 100644 --- a/.gitignore +++ b/.gitignore @@ -20,8 +20,8 @@ erl_crash.dump # Desktop shell (slices 001, 100, 101) # `mix ex_tauri.install` creates `src-tauri/`, not `tauri/`, so the rule this line replaces # named a directory that has never existed. Corrected at slice 001 G4. Rust build output was -# never actually at risk — the generator writes its own src-tauri/.gitignore carrying -# `/target/` — but a rule that matches nothing is a claim that is not true. +# never actually at risk (the generator writes its own src-tauri/.gitignore carrying +# `/target/`), but a rule that matches nothing is a claim that is not true. /src-tauri/target/ /priv/static/assets/ # Burrito's output: one packaged binary per target, tens of MB each. Slice 001 measures them diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index 73a6e08..0000000 --- a/AGENTS.md +++ /dev/null @@ -1,449 +0,0 @@ -This is a web application written using the Phoenix web framework. - -## Project guidelines - -- Use `mix precommit` alias when you are done with all changes and fix any pending issues -- Use the already included and available `:req` (`Req`) library for HTTP requests, **avoid** `:httpoison`, `:tesla`, and `:httpc`. Req is included by default and is the preferred HTTP client for Phoenix apps - -### Phoenix v1.8 guidelines - -- **Always** begin your LiveView templates with `` which wraps all inner content -- The `MyAppWeb.Layouts` module is aliased in the `my_app_web.ex` file, so you can use it without needing to alias it again -- Anytime you run into errors with no `current_scope` assign: - - You failed to follow the Authenticated Routes guidelines, or you failed to pass `current_scope` to `` - - **Always** fix the `current_scope` error by moving your routes to the proper `live_session` and ensure you pass `current_scope` as needed -- Phoenix v1.8 moved the `<.flash_group>` component to the `Layouts` module. You are **forbidden** from calling `<.flash_group>` outside of the `layouts.ex` module -- Out of the box, `core_components.ex` imports an `<.icon name="hero-x-mark" class="w-5 h-5"/>` component for hero icons. **Always** use the `<.icon>` component for icons, **never** use `Heroicons` modules or similar -- **Always** use the imported `<.input>` component for form inputs from `core_components.ex` when available. `<.input>` is imported and using it will save steps and prevent errors -- If you override the default input classes (`<.input class="myclass px-2 py-1 rounded-lg">)`) class with your own values, no default classes are inherited, so your -custom classes must fully style the input - -### JS and CSS guidelines - -- **Use Tailwind CSS classes and custom CSS rules** to create polished, responsive, and visually stunning interfaces. -- Tailwindcss v4 **no longer needs a tailwind.config.js** and uses a new import syntax in `app.css`: - - @import "tailwindcss" source(none); - @source "../css"; - @source "../js"; - @source "../../lib/my_app_web"; - -- **Always use and maintain this import syntax** in the app.css file for projects generated with `phx.new` -- **Never** use `@apply` when writing raw css -- **Always** manually write your own tailwind-based components instead of using daisyUI for a unique, world-class design -- Out of the box **only the app.js and app.css bundles are supported** - - You cannot reference an external vendor'd script `src` or link `href` in the layouts - - You must import the vendor deps into app.js and app.css to use them - - **Never write inline tags within templates** - -### UI/UX & design guidelines - -- **Produce world-class UI designs** with a focus on usability, aesthetics, and modern design principles -- Implement **subtle micro-interactions** (e.g., button hover effects, and smooth transitions) -- Ensure **clean typography, spacing, and layout balance** for a refined, premium look -- Focus on **delightful details** like hover effects, loading states, and smooth page transitions - - - - - -## Elixir guidelines - -- Elixir lists **do not support index based access via the access syntax** - - **Never do this (invalid)**: - - i = 0 - mylist = ["blue", "green"] - mylist[i] - - Instead, **always** use `Enum.at`, pattern matching, or `List` for index based list access, ie: - - i = 0 - mylist = ["blue", "green"] - Enum.at(mylist, i) - -- Elixir variables are immutable, but can be rebound, so for block expressions like `if`, `case`, `cond`, etc - you *must* bind the result of the expression to a variable if you want to use it and you CANNOT rebind the result inside the expression, ie: - - # INVALID: we are rebinding inside the `if` and the result never gets assigned - if connected?(socket) do - socket = assign(socket, :val, val) - end - - # VALID: we rebind the result of the `if` to a new variable - socket = - if connected?(socket) do - assign(socket, :val, val) - end - -- **Never** nest multiple modules in the same file as it can cause cyclic dependencies and compilation errors -- **Never** use map access syntax (`changeset[:field]`) on structs as they do not implement the Access behaviour by default. For regular structs, you **must** access the fields directly, such as `my_struct.field` or use higher level APIs that are available on the struct if they exist, `Ecto.Changeset.get_field/2` for changesets -- Elixir's standard library has everything necessary for date and time manipulation. Familiarize yourself with the common `Time`, `Date`, `DateTime`, and `Calendar` interfaces by accessing their documentation as necessary. **Never** install additional dependencies unless asked or for date/time parsing (which you can use the `date_time_parser` package) -- Don't use `String.to_atom/1` on user input (memory leak risk) -- Predicate function names should not start with `is_` and should end in a question mark. Names like `is_thing` should be reserved for guards -- Elixir's builtin OTP primitives like `DynamicSupervisor` and `Registry`, require names in the child spec, such as `{DynamicSupervisor, name: MyApp.MyDynamicSup}`, then you can use `DynamicSupervisor.start_child(MyApp.MyDynamicSup, child_spec)` -- Use `Task.async_stream(collection, callback, options)` for concurrent enumeration with back-pressure. The majority of times you will want to pass `timeout: :infinity` as option - -## Mix guidelines - -- Read the docs and options before using tasks (by using `mix help task_name`) -- To debug test failures, run tests in a specific file with `mix test test/my_test.exs` or run all previously failed tests with `mix test --failed` -- `mix deps.clean --all` is **almost never needed**. **Avoid** using it unless you have good reason - -## Test guidelines - -- **Always use `start_supervised!/1`** to start processes in tests as it guarantees cleanup between tests -- **Avoid** `Process.sleep/1` and `Process.alive?/1` in tests - - Instead of sleeping to wait for a process to finish, **always** use `Process.monitor/1` and assert on the DOWN message: - - ref = Process.monitor(pid) - assert_receive {:DOWN, ^ref, :process, ^pid, :normal} - - - Instead of sleeping to synchronize before the next call, **always** use `_ = :sys.get_state/1` to ensure the process has handled prior messages - - - -## Phoenix guidelines - -- Remember Phoenix router `scope` blocks include an optional alias which is prefixed for all routes within the scope. **Always** be mindful of this when creating routes within a scope to avoid duplicate module prefixes. - -- You **never** need to create your own `alias` for route definitions! The `scope` provides the alias, ie: - - scope "/admin", AppWeb.Admin do - pipe_through :browser - - live "/users", UserLive, :index - end - - the UserLive route would point to the `AppWeb.Admin.UserLive` module - -- `Phoenix.View` no longer is needed or included with Phoenix, don't use it - - - -## Ecto Guidelines - -- **Always** preload Ecto associations in queries when they'll be accessed in templates, ie a message that needs to reference the `message.user.email` -- Remember `import Ecto.Query` and other supporting modules when you write `seeds.exs` -- `Ecto.Schema` fields always use the `:string` type, even for `:text`, columns, ie: `field :name, :string` -- `Ecto.Changeset.validate_number/2` **DOES NOT SUPPORT the `:allow_nil` option**. By default, Ecto validations only run if a change for the given field exists and the change value is not nil, so such as option is never needed -- You **must** use `Ecto.Changeset.get_field(changeset, :field)` to access changeset fields -- Fields which are set programmatically, such as `user_id`, must not be listed in `cast` calls or similar for security purposes. Instead they must be explicitly set when creating the struct -- **Always** invoke `mix ecto.gen.migration migration_name_using_underscores` when generating migration files, so the correct timestamp and conventions are applied - - - -## Phoenix HTML guidelines - -- Phoenix templates **always** use `~H` or .html.heex files (known as HEEx), **never** use `~E` -- **Always** use the imported `Phoenix.Component.form/1` and `Phoenix.Component.inputs_for/1` function to build forms. **Never** use `Phoenix.HTML.form_for` or `Phoenix.HTML.inputs_for` as they are outdated -- When building forms **always** use the already imported `Phoenix.Component.to_form/2` (`assign(socket, form: to_form(...))` and `<.form for={@form} id="msg-form">`), then access those forms in the template via `@form[:field]` -- **Always** add unique DOM IDs to key elements (like forms, buttons, etc) when writing templates, these IDs can later be used in tests (`<.form for={@form} id="product-form">`) -- For "app wide" template imports, you can import/alias into the `my_app_web.ex`'s `html_helpers` block, so they will be available to all LiveViews, LiveComponent's, and all modules that do `use MyAppWeb, :html` (replace "my_app" by the actual app name) - -- Elixir supports `if/else` but **does NOT support `if/else if` or `if/elsif`**. **Never use `else if` or `elseif` in Elixir**, **always** use `cond` or `case` for multiple conditionals. - - **Never do this (invalid)**: - - <%= if condition do %> - ... - <% else if other_condition %> - ... - <% end %> - - Instead **always** do this: - - <%= cond do %> - <% condition -> %> - ... - <% condition2 -> %> - ... - <% true -> %> - ... - <% end %> - -- HEEx require special tag annotation if you want to insert literal curly's like `{` or `}`. If you want to show a textual code snippet on the page in a `
` or `` block you *must* annotate the parent tag with `phx-no-curly-interpolation`:
-
-      
-        let obj = {key: "val"}
-      
-
-  Within `phx-no-curly-interpolation` annotated tags, you can use `{` and `}` without escaping them, and dynamic Elixir expressions can still be used with `<%= ... %>` syntax
-
-- HEEx class attrs support lists, but you must **always** use list `[...]` syntax. You can use the class list syntax to conditionally add classes, **always do this for multiple class values**:
-
-      Text
-
-  and **always** wrap `if`'s inside `{...}` expressions with parens, like done above (`if(@other_condition, do: "...", else: "...")`)
-
-  and **never** do this, since it's invalid (note the missing `[` and `]`):
-
-       ...
-      => Raises compile syntax error on invalid HEEx attr syntax
-
-- **Never** use `<% Enum.each %>` or non-for comprehensions for generating template content, instead **always** use `<%= for item <- @collection do %>`
-- HEEx HTML comments use `<%!-- comment --%>`. **Always** use the HEEx HTML comment syntax for template comments (`<%!-- comment --%>`)
-- HEEx allows interpolation via `{...}` and `<%= ... %>`, but the `<%= %>` **only** works within tag bodies. **Always** use the `{...}` syntax for interpolation within tag attributes, and for interpolation of values within tag bodies. **Always** interpolate block constructs (if, cond, case, for) within tag bodies using `<%= ... %>`.
-
-  **Always** do this:
-
-      
- {@my_assign} - <%= if @some_block_condition do %> - {@another_assign} - <% end %> -
- - and **Never** do this – the program will terminate with a syntax error: - - <%!-- THIS IS INVALID NEVER EVER DO THIS --%> -
- {if @invalid_block_construct do} - {end} -
- - - -## Phoenix LiveView guidelines - -- **Never** use the deprecated `live_redirect` and `live_patch` functions, instead **always** use the `<.link navigate={href}>` and `<.link patch={href}>` in templates, and `push_navigate` and `push_patch` functions LiveViews -- **Avoid LiveComponent's** unless you have a strong, specific need for them -- LiveViews should be named like `AppWeb.WeatherLive`, with a `Live` suffix. When you go to add LiveView routes to the router, the default `:browser` scope is **already aliased** with the `AppWeb` module, so you can just do `live "/weather", WeatherLive` - -### LiveView streams - -- **Always** use LiveView streams for collections for assigning regular lists to avoid memory ballooning and runtime termination with the following operations: - - basic append of N items - `stream(socket, :messages, [new_msg])` - - resetting stream with new items - `stream(socket, :messages, [new_msg], reset: true)` (e.g. for filtering items) - - prepend to stream - `stream(socket, :messages, [new_msg], at: -1)` - - deleting items - `stream_delete(socket, :messages, msg)` - -- When using the `stream/3` interfaces in the LiveView, the LiveView template must 1) always set `phx-update="stream"` on the parent element, with a DOM id on the parent element like `id="messages"` and 2) consume the `@streams.stream_name` collection and use the id as the DOM id for each child. For a call like `stream(socket, :messages, [new_msg])` in the LiveView, the template would be: - -
-
- {msg.text} -
-
- -- LiveView streams are *not* enumerable, so you cannot use `Enum.filter/2` or `Enum.reject/2` on them. Instead, if you want to filter, prune, or refresh a list of items on the UI, you **must refetch the data and re-stream the entire stream collection, passing reset: true**: - - def handle_event("filter", %{"filter" => filter}, socket) do - # re-fetch the messages based on the filter - messages = list_messages(filter) - - {:noreply, - socket - |> assign(:messages_empty?, messages == []) - # reset the stream with the new messages - |> stream(:messages, messages, reset: true)} - end - -- LiveView streams *do not support counting or empty states*. If you need to display a count, you must track it using a separate assign. For empty states, you can use Tailwind classes: - -
- -
- {task.name} -
-
- - The above only works if the empty state is the only HTML block alongside the stream for-comprehension. - -- When updating an assign that should change content inside any streamed item(s), you MUST re-stream the items - along with the updated assign: - - def handle_event("edit_message", %{"message_id" => message_id}, socket) do - message = Chat.get_message!(message_id) - edit_form = to_form(Chat.change_message(message, %{content: message.content})) - - # re-insert message so @editing_message_id toggle logic takes effect for that stream item - {:noreply, - socket - |> stream_insert(:messages, message) - |> assign(:editing_message_id, String.to_integer(message_id)) - |> assign(:edit_form, edit_form)} - end - - And in the template: - -
-
- {message.username} - <%= if @editing_message_id == message.id do %> - <%!-- Edit mode --%> - <.form for={@edit_form} id="edit-form-#{message.id}" phx-submit="save_edit"> - ... - - <% end %> -
-
- -- **Never** use the deprecated `phx-update="append"` or `phx-update="prepend"` for collections - -### LiveView JavaScript interop - -- Remember anytime you use `phx-hook="MyHook"` and that JS hook manages its own DOM, you **must** also set the `phx-update="ignore"` attribute -- **Always** provide an unique DOM id alongside `phx-hook` otherwise a compiler error will be raised - -LiveView hooks come in two flavors, 1) colocated js hooks for "inline" scripts defined inside HEEx, -and 2) external `phx-hook` annotations where JavaScript object literals are defined and passed to the `LiveSocket` constructor. - -#### Inline colocated js hooks - -**Never** write raw embedded ` - -- colocated hooks are automatically integrated into the app.js bundle -- colocated hooks names **MUST ALWAYS** start with a `.` prefix, i.e. `.PhoneNumber` - -#### External phx-hook - -External JS hooks (`
`) must be placed in `assets/js/` and passed to the -LiveSocket constructor: - - const MyHook = { - mounted() { ... } - } - let liveSocket = new LiveSocket("/live", Socket, { - hooks: { MyHook } - }); - -#### Pushing events between client and server - -Use LiveView's `push_event/3` when you need to push events/data to the client for a phx-hook to handle. -**Always** return or rebind the socket on `push_event/3` when pushing events: - - # re-bind socket so we maintain event state to be pushed - socket = push_event(socket, "my_event", %{...}) - - # or return the modified socket directly: - def handle_event("some_event", _, socket) do - {:noreply, push_event(socket, "my_event", %{...})} - end - -Pushed events can then be picked up in a JS hook with `this.handleEvent`: - - mounted() { - this.handleEvent("my_event", data => console.log("from server:", data)); - } - -Clients can also push an event to the server and receive a reply with `this.pushEvent`: - - mounted() { - this.el.addEventListener("click", e => { - this.pushEvent("my_event", { one: 1 }, reply => console.log("got reply from server:", reply)); - }) - } - -Where the server handled it via: - - def handle_event("my_event", %{"one" => 1}, socket) do - {:reply, %{two: 2}, socket} - end - -### LiveView tests - -- `Phoenix.LiveViewTest` module and `LazyHTML` (included) for making your assertions -- Form tests are driven by `Phoenix.LiveViewTest`'s `render_submit/2` and `render_change/2` functions -- Come up with a step-by-step test plan that splits major test cases into small, isolated files. You may start with simpler tests that verify content exists, gradually add interaction tests -- **Always reference the key element IDs you added in the LiveView templates in your tests** for `Phoenix.LiveViewTest` functions like `element/2`, `has_element/2`, selectors, etc -- **Never** tests again raw HTML, **always** use `element/2`, `has_element/2`, and similar: `assert has_element?(view, "#my-form")` -- Instead of relying on testing text content, which can change, favor testing for the presence of key elements -- Focus on testing outcomes rather than implementation details -- Be aware that `Phoenix.Component` functions like `<.form>` might produce different HTML than expected. Test against the output HTML structure, not your mental model of what you expect it to be -- When facing test failures with element selectors, add debug statements to print the actual HTML, but use `LazyHTML` selectors to limit the output, ie: - - html = render(view) - document = LazyHTML.from_fragment(html) - matches = LazyHTML.filter(document, "your-complex-selector") - IO.inspect(matches, label: "Matches") - -### Form handling - -#### Creating a form from params - -If you want to create a form based on `handle_event` params: - - def handle_event("submitted", params, socket) do - {:noreply, assign(socket, form: to_form(params))} - end - -When you pass a map to `to_form/1`, it assumes said map contains the form params, which are expected to have string keys. - -You can also specify a name to nest the params: - - def handle_event("submitted", %{"user" => user_params}, socket) do - {:noreply, assign(socket, form: to_form(user_params, as: :user))} - end - -#### Creating a form from changesets - -When using changesets, the underlying data, form params, and errors are retrieved from it. The `:as` option is automatically computed too. E.g. if you have a user schema: - - defmodule MyApp.Users.User do - use Ecto.Schema - ... - end - -And then you create a changeset that you pass to `to_form`: - - %MyApp.Users.User{} - |> Ecto.Changeset.change() - |> to_form() - -Once the form is submitted, the params will be available under `%{"user" => user_params}`. - -In the template, the form form assign can be passed to the `<.form>` function component: - - <.form for={@form} id="todo-form" phx-change="validate" phx-submit="save"> - <.input field={@form[:field]} type="text" /> - - -Always give the form an explicit, unique DOM ID, like `id="todo-form"`. - -#### Avoiding form errors - -**Always** use a form assigned via `to_form/2` in the LiveView, and the `<.input>` component in the template. In the template **always access forms this**: - - <%!-- ALWAYS do this (valid) --%> - <.form for={@form} id="my-form"> - <.input field={@form[:field]} type="text" /> - - -And **never** do this: - - <%!-- NEVER do this (invalid) --%> - <.form for={@changeset} id="my-form"> - <.input field={@changeset[:field]} type="text" /> - - -- You are FORBIDDEN from accessing the changeset in the template as it will cause errors -- **Never** use `<.form let={f} ...>` in the template, instead **always use `<.form for={@form} ...>`**, then drive all form references from the form assign as in `@form[:field]`. The UI should **always** be driven by a `to_form/2` assigned in the LiveView module that is derived from a changeset - - - \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index d271ec9..243e7a4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,11 +1,11 @@ -# CLAUDE.md — Operating rules for the coding agent +# CLAUDE.md: operating rules for the coding agent You are the implementation engineer on **Trinity**, a personal AI agent built on Elixir/OTP + Phoenix LiveView, packaged as a desktop app. A human product owner reviews your work slice by slice. This file is the contract. ## 0. Read order at the start of every session -1. `ROADMAP.md` — find the current slice (status `in_progress`) or the next `ready` slice. +1. `ROADMAP.md`: find the current slice (status `in_progress`) or the next `ready` slice. 2. `slices/NNN-*/SLICE.md` for that slice. Read it fully. Read its `NOTES.md` if it exists. 3. `docs/03-conventions.md` and `docs/04-slice-process.md` (skim; they do not change often). 4. `docs/01-architecture.md` section relevant to the slice. @@ -46,7 +46,7 @@ mix gate # the full quality gate (defined in Slice 000). Mu mix test # tests mix test --cover # coverage (report the line in PROOF.md) mix credo --strict -mix boundary # run via `mix compile` — boundary violations are compile warnings → errors +mix boundary # run via `mix compile`: boundary violations are compile warnings → errors mix hex.audit && mix deps.audit mix versions.verify # (Slice 000) prints installed vs VERSIONS.md ``` @@ -58,10 +58,12 @@ mix versions.verify # (Slice 000) prints installed vs VERSIONS.md `feat(s012): session GenServer with restart rehydration` `test(s012): crash-recovery test for Session` `docs(s012): PROOF.md` -- Final commit of a slice: `feat(s012): complete slice 012 — session process and agent loop`. +- Final commit of a slice: `feat(s012): complete slice 012 (session process and agent loop)`. It must include `PROOF.md` and the `ROADMAP.md` status change. -- Merge to `main` with `git merge --no-ff slice/NNN-short-name` (keeps the slice boundary visible), then - `git tag -a slice/NNN -m "Slice NNN: "`. +- Merge to `main` through a pull request. The repository ruleset requires one, allows only the merge-commit + method (the slice boundary stays visible, as `--no-ff` did), and requires the `gate` check green on the + branch head. Nobody can bypass it, the owner included. After the merge, on `main`: + `git tag -a slice/NNN -m "Slice NNN: <title>"` and push the tag. Tags cannot be moved or deleted. - Every commit is DCO signed-off (`git commit -s`); the hook and CI refuse otherwise (ADR-0012). - Never force-push `main`. Never rewrite tagged history. - Never commit secrets. `.env*` is gitignored. API keys come from env or the OS keychain module. @@ -69,7 +71,7 @@ mix versions.verify # (Slice 000) prints installed vs VERSIONS.md ## 5. Engineering rules - **Modularity:** every pluggable concern is a `@behaviour` behind a registry. Adding a tool/provider/gateway - must never require editing a core module — only adding a module and a config entry. + must never require editing a core module: only adding a module and a config entry. - **Boundaries:** respect `docs/01-architecture.md` dependency rules. `boundary` enforces them at compile time. - **OTP first:** one process per session; supervise everything; no bare `spawn`; use `Task.Supervisor`. - **No `Code.eval_string` on model output.** Ever. Sandboxed execution goes through `Trinity.Sandbox`. @@ -119,10 +121,10 @@ discovering it when the slice is otherwise finished is discovering it too late. - **Every egress gets a redaction row.** Anything Trinity sends off-machine (proposals, telemetry, MCP results to other agents) is enumerated with what crosses raw and what crosses hashed. - **MCP is versioned by date, never by a major number.** Write "AARM" as the category. Commercial names belong on - public surfaces; code names live only in identifiers. Real names appear only where the name check permits them — + public surfaces; code names live only in identifiers. Real names appear only where the name check permits them: the enforcer is the rule, and no prose here overrides it. ## 9. Tone of PROOF.md and NOTES.md -Plain, factual, first person allowed. Report failures and workarounds honestly — the human is grading +Plain, factual, first person allowed. Report failures and workarounds honestly: the human is grading accuracy of the report as much as the code. diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index e46b072..1bcb8df 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -16,7 +16,7 @@ Project spaces, and anywhere someone is representing the project. ## Reporting -Email **security@scriptkittyos.com** — the same address, because there is one maintainer and a +Email **security@scriptkittyos.com**: the same address, because there is one maintainer and a second address would be theatre. Reports are read by that maintainer. If your report concerns the maintainer, say so in the subject; there is no independent body today, and pretending otherwise would be worse than admitting it. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c4af550..3af6ed9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,38 +1,63 @@ <!-- SPDX-License-Identifier: Apache-2.0 --> # Contributing -## Before anything +Trinity is pre-alpha with one maintainer, and the work is planned as numbered slices +(`ROADMAP.md`). The most useful contributions right now are bug reports against a tagged slice, +corrections to the documents, and review of a slice's proof. If you want to build a slice, open +an issue naming it first so two people do not build the same one. + +## Setup ``` -git config core.hooksPath .githooks +asdf install # reads .tool-versions +git config core.hooksPath .githooks # the commit-msg hook; see below +mix setup +mix gate ``` -`.githooks/commit-msg` strips assistant attribution trailers. `scripts/plan_check.sh` checks -the history too, so an unconfigured hook fails the gate rather than passing quietly. +`mix gate` has to exit 0 before every commit. It is the same command locally and in CI. -## The rules that actually bind +## The rules that bind -`CLAUDE.md` is the contract; `docs/04-slice-process.md` is the process. The short version: +`CLAUDE.md` is the engineering contract and `docs/04-slice-process.md` is the process. The +short version: -- **One slice at a time**, on `slice/NNN-short-name`. Do not widen scope. -- **`mix gate` must pass before every commit**, and `scripts/plan_check.sh` with it. -- **Every commit is signed off** (`git commit -s`). The hook and the gate refuse otherwise. -- **Proof means the command you ran and the output it produced.** Not a summary of it, not a - reformatted digest inside a `$` block. A "verified" names its command and its exit code. -- **Populations derive from the tree.** Any "every X" names the command that enumerates X. -- **Corrections append.** Records are never rewritten; a wrong line stays and is corrected - below it, saying what it supersedes. -- **A red before a fix.** A test for a claimed property is committed failing first, by name. +- **One slice at a time**, on a branch named `slice/NNN-short-name`. Do not widen its scope; write + anything you find outside it under "Follow-ups" in that slice's `NOTES.md`. +- **`mix gate` passes before every commit**, including `scripts/plan_check.sh`. +- **Every commit is signed off** (`git commit -s`, the Developer Certificate of Origin). The hook + and the gate refuse a commit without it. +- **No attribution trailers in commit messages.** `.githooks/commit-msg` strips them, and + `plan_check` checks the history itself, so an unconfigured hook fails the gate rather than + passing quietly. Commit messages also carry no issue-tracker identifiers. +- **Proof is the command you ran and the output it produced.** Not a summary of it. Anything + described as verified names its command and its exit code. +- **Populations come from the tree.** Any claim about "every X" names the command that lists X. +- **Corrections are appended.** `PROOF.md`, `NOTES.md` and the decision records are never + rewritten; a wrong line stays and is corrected below it, saying what it supersedes. +- **A failing test comes before the fix.** A test for a claimed property is committed failing + first, by name, and the fix commit refers to it. -## Setup +## Commit messages + +Conventional Commits with the slice id as the scope: ``` -asdf install # reads .tool-versions -mix setup -mix gate +feat(s012): session process with restart rehydration +test(s012): crash-recovery test for the session process +docs(s012): proof ``` -## What gets a change rejected +The final commit of a slice reads `feat(s012): complete slice 012 (session process and agent loop)` +and includes `PROOF.md` and the `ROADMAP.md` status change. Slices merge to `main` through a +pull request with the `gate` check green, merge-commit method only, and are then tagged `slice/NNN`. + +## What gets a change sent back + +An unproven claim. A count typed rather than derived. A skipped check without a stated reason. +A rule added as prose where an enforcer was possible. A version not listed in `VERSIONS.md` +(propose the newer one in `NOTES.md` instead of upgrading mid-slice). + +## Reporting problems -An unproven claim. A count typed rather than derived. A skip without a stated reason. A rule -added as prose where an enforcer was possible. +Bugs and questions go in the issue tracker. Security reports do not; see `SECURITY.md`. diff --git a/GOVERNANCE.md b/GOVERNANCE.md index ccbeaf1..45fd374 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -11,7 +11,7 @@ stated rather than dressed up: the review queue is the project's real critical p Anything that changes architecture, stack, data model or process gets an ADR in `docs/adr/`, with a status of `proposed`, `accepted`, or `superseded by ADR-XXXX`. Decisions are recorded -before they are implemented, and corrections are **appended** — an ADR is never rewritten to +before they are implemented, and corrections are **appended**: an ADR is never rewritten to look as though it had always been right. ## Becoming a committer diff --git a/README.md b/README.md index dc4de42..bff86ea 100644 --- a/README.md +++ b/README.md @@ -1,73 +1,115 @@ -# Trinity — Project Plan Package +<!-- SPDX-License-Identifier: Apache-2.0 --> +# Trinity -This directory is the **planning and accountability layer** for building Trinity: a personal AI agent on -Elixir/BEAM with Phoenix LiveView, shipped as a desktop app. It runs on your machine, remembers you, learns -procedures, acts through tools under a permission gate, and reaches you on any surface. +A personal AI agent that runs on your own machine. It remembers you, learns procedures, acts +through tools under a permission gate, reaches you on whatever surface you are using, and does +not lose your work when something crashes. -It is designed to be consumed by a coding agent with a human acting as product owner. +Trinity is built on Elixir and the BEAM, with Phoenix LiveView for the interface, and ships as a +desktop application. Apache-2.0, developed in the open from the first commit. -## How to use this package +## Status -1. Point git at the tracked hooks, before the first commit: +Pre-alpha. The repository, quality gate and packaging path are in place and proven (milestone M0). +There is no chat, no model integration and no tool execution yet; those arrive with milestones +M1 and M2. `ROADMAP.md` carries the live status of every slice of work, and the +[Milestones](#milestones) section below explains how to read it. - ``` - git config core.hooksPath .githooks - ``` +Nothing here is ready to use. If you want to follow along, watch the roadmap and the tags. - `.githooks/commit-msg` strips assistant attribution trailers from every message. It is the first step - because a trailer that reaches a tag is permanent. `scripts/plan_check.sh` rule 8 checks the history - itself, so an unconfigured hook fails the gate rather than passing quietly. -2. Create an empty git repo for the project. -3. Copy these files and directories into the repo root, and only these: - - `CLAUDE.md` (the coding agent reads it automatically) - - `ROADMAP.md` - - `VERSIONS.md` - - `docs/` - - `templates/` - - `slices/` -4. Commit: `chore: add project plan package` — this is commit #1, before any code. -5. Tell the coding agent: **"Start Slice 000."** Everything else is in `CLAUDE.md`. -6. After each slice, you review `slices/NNN-*/PROOF.md`, then say "approve slice NNN" or list changes. - The agent does not begin the next slice without your approval (see `docs/04-slice-process.md`). +## Why the BEAM -## Running the app +Agents of this shape tend to fail in the same few ways: one synchronous loop that exits and loses +a run, database corruption when two processes write the same file, memory that degrades every +time it is compressed, and a note in the documentation asking you not to run two copies at once. +Those are failures of the substrate, not of the product. The BEAM was built to remove that class +of failure, so on it these properties can be structural rather than aspirational: -Requires the pinned toolchain in `.tool-versions` (Erlang 28.5.0.5, Elixir 1.20.4-otp-28), installed with -`asdf install`. +- A crash in one session, tool or gateway never affects another and never loses persisted state. +- One agent, seen on the desktop, in Telegram and in Discord at the same time, in real time. +- Memory in tiers: a small always-on set of facts plus unlimited retrievable history. +- Every side-effecting action passes a permission gate, and dangerous ones need explicit approval. +- Any model or provider, cloud or local, switched by configuration. + +`docs/00-vision.md` states the goals, the non-goals and the properties the design has to +demonstrate, each tied to the slice that proves it. + +## Running from source + +Requires the pinned toolchain in `.tool-versions` (Erlang 28.5.0.5, Elixir 1.20.4, Zig 0.16.0), +installed with `asdf install`. Rust 1.92.0 is pinned separately in `rust-toolchain.toml` and is +only needed for the desktop shell. ``` -mix setup # deps, database, assets -mix phx.server # or: iex -S mix phx.server -mix gate # the full quality gate — must pass before every commit +git config core.hooksPath .githooks # once, before your first commit +mix setup # dependencies, database, assets +mix phx.server # or: iex -S mix phx.server ``` -Then visit [`localhost:4000`](http://localhost:4000). +Then open [localhost:4000](http://localhost:4000). Today that is a scaffold page, not an agent. + +`mix gate` runs the full quality gate and has to pass before every commit: format check, compile +with warnings as errors, Credo, Sobelow, dependency audits, version verification, the naming and +secret checks, the tests with coverage, and `scripts/plan_check.sh`, which checks the plan +documents themselves for consistency. + +Packaging as a single binary is documented in `docs/packaging.md`, with measured sizes and +start-up times for each target. + +## How the work is organised + +Trinity is built in slices. A slice is one unit of planning, work, proof, review and history: +small enough to review in one sitting and large enough to deserve a tag. + +Each slice has a folder under `slices/` holding its specification (`SLICE.md`), the working notes +and deviations recorded while it was built (`NOTES.md`), and the evidence that it met its +acceptance criteria (`PROOF.md`). Proof means the command that was run and the output it produced, +pasted in, or a screenshot for anything visual. A sentence saying something works is not proof. + +A slice moves through `planned`, `ready`, `in_progress`, `done` and `approved`. Only the +maintainer sets `approved`, after reading the proof. Each approved slice is merged with a merge +commit and tagged `slice/NNN`, so the history is the audit log. -`mix gate` is the contract: format check, compile with warnings as errors, Credo, sobelow, dependency audits, -tests, the secret scan and the five rule enforcers. `scripts/plan_check.sh` runs alongside it and checks the plan -itself — acceptance-criteria numbering, roadmap agreement, dangling references, commit-message hygiene. +Two rules shape everything else. Records are appended to and never rewritten: a wrong line stays +where it is and is corrected below it, saying what it supersedes. And a count, a hash, a date or +a version is never typed from memory; it is derived from the tree by a command that is named next +to it. -## What is in here +`docs/04-slice-process.md` has the full lifecycle and the review gates. `CLAUDE.md` is the +engineering contract that every change is held to. + +## Milestones + +| Milestone | Meaning | Reached when | +|---|---|---| +| M0 Stands | Repository, quality gate and packaging path proven | 000 and 001 approved | +| M1 Talks | Streaming chat with any provider, persisted and crash-safe | 010 to 013 approved | +| M2 Acts | Tools behind a permission gate, one side-effect membrane, local receipts, context compaction | 020 to 024 approved | +| M3 Remembers | Persona, always-on memory, full-text and semantic recall, project context, export and import | 030 to 034 approved | +| M4 Learns | A skills system the agent can extend itself, behind approval and a scanner | 040 and 041 approved | +| M5a Automates | Scheduled tasks and MCP, client and server, with authorization | 050 and 059 to 062 approved | +| M5b Reaches | Messaging gateways and subagents | 070 to 072 and 080 approved | +| M6 Ships | Observability and a cost ledger, native desktop shell, signed releases | 090 to 101 approved | +| M7 Sandboxed | Executable skills in an in-VM sandbox | 110 approved | +| M9 Donatable | Open-source hygiene audited, supply chain signed, shared libraries extracted | 120 to 123 approved | + +Slice numbers have gaps on purpose (000, 001, 010, 011 and so on) so that a slice can be inserted +later without renumbering anything. + +## What is in the repository | Path | Purpose | |---|---| -| `CLAUDE.md` | Operating rules for the coding agent. Non-negotiable process. | -| `ROADMAP.md` | Every slice, its phase, milestone, dependencies, and live status. | -| `VERSIONS.md` | Verified dependency versions + the re-verification procedure. | -| `docs/00-vision.md` | Goals, non-goals, principles, and the properties this design is meant to demonstrate. | -| `docs/01-architecture.md` | Supervision tree, module map, extension points, data flow. | -| `docs/02-tech-stack.md` | Library choices with justification and risk flags. | -| `docs/03-conventions.md` | Code style, branching, commits, tests, proof standard, Definition of Done. | -| `docs/04-slice-process.md` | The slice lifecycle and the gates a slice must pass. | -| `docs/05-data-model.md` | Ecto schemas and invariants. | -| `docs/06-risk-register.md` | Known risks, triggers, mitigations, owners. | -| `docs/07-security-model.md` | Trust boundaries, permission gate, secrets, injection defences. | -| `docs/08-standards.md` | Standards landscape (AAIF, MCP 2026-07-28, A2A, Agent Skills, AGENTS.md) and Trinity's posture. | -| `docs/adr/` | Architecture Decision Records. Add one whenever a decision changes. | -| `templates/` | SLICE, PROOF and ADR templates. | -| `slices/NNN-name/SLICE.md` | The spec for each slice (goal, scope, acceptance criteria, proof required). | -| `slices/NNN-name/PROOF.md` | Written by the agent when the slice is done. Evidence, not claims. | -| `slices/NNN-name/NOTES.md` | Optional. Deviations, decisions, follow-ups discovered during the slice. | +| `ROADMAP.md` | Every slice with its phase, milestone, dependencies and current status | +| `VERSIONS.md` | The verified dependency versions, generated from `lib/trinity/versions.ex` | +| `CLAUDE.md` | The engineering contract: slice rules, definition of done, proof standard | +| `docs/` | Vision, architecture, tech stack, conventions, slice process, data model, risks, security model, standards | +| `docs/adr/` | Architecture decision records. One is added whenever a decision changes | +| `slices/` | One folder per slice: specification, notes and proof | +| `templates/` | The templates a new slice, proof or decision record starts from | +| `lib/`, `test/`, `config/` | The application | +| `src-tauri/` | The native desktop shell | +| `scripts/`, `credo_checks/` | The plan checker and this project's own Credo checks | ## Connecting Trinity to the platform @@ -85,15 +127,21 @@ coming back from it is tagged untrusted like any other external content. Both are optional. `TRINITY_AUTHORITY=local` with no MCP servers configured is a complete Trinity. -## Principles baked into this plan - -- **Accountability by artifact.** A slice is done when `PROOF.md` shows the gate passed and the acceptance - criteria are demonstrated with command output, not prose. -- **One slice → one merge → one tag.** History is the audit log. -- **De-risk early.** Slice 001 is a packaging spike, because desktop packaging is the biggest unknown. -- **Modular by construction.** Every pluggable thing (LLM provider, tool, gateway, memory store, - skill loader) is a behaviour behind a registry, enforced with the `boundary` library. -- **Latest *stable* versions, verified, not assumed.** `VERSIONS.md` is re-verified at Slice 000 and at - every phase boundary. Note the packaging-driven OTP pin (see `docs/adr/0005-*.md`). -- **Numbering gaps are intentional.** Slices are numbered 000, 001, 010, 011… so new slices can be inserted - without renumbering. +## Contributing, security and governance + +See `CONTRIBUTING.md` for how a change gets in, `SECURITY.md` for how to report a vulnerability, +and `GOVERNANCE.md` and `MAINTAINERS.md` for who decides what. `CODE_OF_CONDUCT.md` applies in +every project space. + +## Related projects + +[beam_mcp](https://github.com/ScriptKittyOS/beam_mcp) is a Model Context Protocol server core for +the BEAM from the same organisation, on Hex as `beam_mcp`. Trinity does not depend on it today. +Trinity's MCP layer (milestone M5a) is chosen by measurement in slice 059, where beam_mcp is one +of the candidates for the server side; the client side and the authorization server are Trinity's +own work whichever library is chosen. `docs/adr/0007-mcp-2026-07-28-target-and-library.md` +records the protocol target and how the choice is made. + +## License + +Apache-2.0. See `LICENSE` and `NOTICE`. diff --git a/ROADMAP.md b/ROADMAP.md index 6dc43be..8c769fa 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,4 +1,4 @@ -# ROADMAP — Trinity +# Roadmap Status values: `planned` → `ready` (deps approved) → `in_progress` → `done` (agent) → `approved` (human). `blocked` is also a status, as `docs/04-slice-process.md` has always defined it, and was missing from this legend. @@ -11,22 +11,22 @@ A size given as `M or L` is conditional on a decision named in that slice's file | Milestone | Meaning | Reached when | |---|---|---| -| **M0 — Stands** | Repo, gate, packaging path proven | 000, 001 approved | -| **M1 — Talks** | Streaming chat with any provider, persisted, crash-safe | 010–013 approved | -| **M2 — Acts** | Tools with permission gate, one side-effect membrane, local receipts, context compaction | 020–024 approved | -| **M3 — Remembers** | Persona, always-on memory, FTS + semantic recall, project context, and data you can take with you | 030–034 approved | -| **M4 — Learns** | Skills system with agent self-management + approval | 040–041 approved | -| **M5a — Automates** | Cron tasks and MCP, client and server, with authorization | 050, 059–062 approved | -| **M5b — Reaches** | Gateways over PubSub, subagents | 070–072, 080 approved (081 optional, outside the milestone) | -| **M6 — Ships** | Observability, native desktop shell, signed releases | 090–101 approved | -| **M7 — Sandboxed** | Executable skills in an in-VM sandbox | 110 approved | -| **M9 — Donatable** | OSS hygiene audited, supply chain signed, AAIF Sandbox package complete, shared libraries extracted | 120–123 approved (122 filing is an owner action) | +| **M0 Stands** | Repo, gate, packaging path proven | 000, 001 approved | +| **M1 Talks** | Streaming chat with any provider, persisted, crash-safe | 010–013 approved | +| **M2 Acts** | Tools with permission gate, one side-effect membrane, local receipts, context compaction | 020–024 approved | +| **M3 Remembers** | Persona, always-on memory, FTS + semantic recall, project context, and data you can take with you | 030–034 approved | +| **M4 Learns** | Skills system with agent self-management + approval | 040–041 approved | +| **M5a Automates** | Cron tasks and MCP, client and server, with authorization | 050, 059–062 approved | +| **M5b Reaches** | Gateways over PubSub, subagents | 070–072, 080 approved (081 optional, outside the milestone) | +| **M6 Ships** | Observability, native desktop shell, signed releases | 090–101 approved | +| **M7 Sandboxed** | Executable skills in an in-VM sandbox | 110 approved | +| **M9 Donatable** | OSS hygiene audited, supply chain signed, AAIF Sandbox package complete, shared libraries extracted | 120–123 approved (122 filing is an owner action) | ## Slices | ID | Slice | Phase | Size | Depends on | Status | |---|---|---|---|---|---| -| 000 | Toolchain, repo bootstrap, quality gate | 0 Foundation | L | — | approved | +| 000 | Toolchain, repo bootstrap, quality gate | 0 Foundation | L | none | approved | | 001 | Packaging spike: Burrito + ex_tauri smoke build | 0 Foundation | M | 000 | approved | | 010 | Core domain + persistence (Ecto/SQLite, schemas, Repo owner) | 1 Core loop | M | 000 | planned | | 011 | LLM provider layer (req_llm behind `Trinity.LLM` behaviour) | 1 Core loop | M | 010 | planned | @@ -54,9 +54,9 @@ A size given as `M or L` is conditional on a decision named in that slice's file | 072 | Gateway: Discord (Nostrum) | 7 Gateways | S | 070 | planned | | 080 | Subagents + delegation | 8 Orchestration | M | 020, 023 | planned | | 081 | A2A v1.0 Agent Card + task intake (optional) | 8 Orchestration | M | 080, 061 | planned (optional) | -| 082 | withdrawn: delegating effects to an external authority layer is the adapter's job, outside this tree | — | — | — | withdrawn | -| 083 | withdrawn: verifying another system's receipts belongs with that system's adapter | — | — | — | withdrawn | -| 084 | withdrawn: connecting to a specific MCP server is configuration, not a slice | — | — | — | withdrawn | +| 082 | withdrawn: delegating effects to an external authority layer is the adapter's job, outside this tree | none | none | none | withdrawn | +| 083 | withdrawn: verifying another system's receipts belongs with that system's adapter | none | none | none | withdrawn | +| 084 | withdrawn: connecting to a specific MCP server is configuration, not a slice | none | none | none | withdrawn | | 090 | Observability: telemetry, cost ledger, LiveDashboard | 1 Core loop | M | 011 | planned | | 100 | Desktop shell: ex_tauri window, tray, notifications, keychain | 10 Desktop | L | 001, 013 | planned | | 101 | Release pipeline: signing, notarization, auto-update | 10 Desktop | L | 100 | planned | diff --git a/VERSIONS.md b/VERSIONS.md index 4c1d037..c2e76c7 100644 --- a/VERSIONS.md +++ b/VERSIONS.md @@ -1,4 +1,4 @@ -# VERSIONS — verified stack +# VERSIONS: the verified stack **Rule:** the coding agent uses these versions. Newer versions are proposed in a slice's `NOTES.md`, approved by the human, then recorded here with a new "verified" date. `mix versions.verify` (Slice 000) diffs `mix.lock` @@ -29,7 +29,7 @@ A ✅ means that command was run and its answer is in the row, with the date. No | ✅ `.tool-versions` | a toolchain component, marked from the pin file rather than from hex | **This replaces the old legend**, under which ✅ meant "someone ran `curl` against hex.pm on the -date in the row". That mark could not be re-derived and outlived the fact twice — finding B3 +date in the row". That mark could not be re-derived and outlived the fact twice: finding B3 caught two false ✅ marks on the two packages the OTP pin rested on. A mark a command produces cannot go stale without the command saying so, and `mix versions.gen --check` is a gate step. @@ -64,14 +64,14 @@ never pin a version hex marks as retired or vulnerable. $ mix versions.verify # asserts every pin is satisfied by mix.lock Marks last derived: 2026-09-06. --> -### Toolchain — each row names its own pin file or command +### Toolchain: each row names its own pin file or command | Name | Pin | Verified | Note | |---|---|---|---| | `Erlang/OTP` | **28.5.0.5** | ✅ `.tool-versions` | Measured at Slice 000, not read from a README: Burrito 1.6.0's ERTS resolver names one artifact source per target, and 28.5.0.5 is the newest OTP returning 200 on all four (macOS universal, Linux x86_64, Linux aarch64, Windows). 28.5.0.6 is released but its macOS and Linux artifacts are unbuilt (404). OTP 29 is 404 on macOS and both Linux arches. ⚠️ Windows tracks OTP releases immediately while the other three lag a third-party CDN's build queue, so re-probe at every phase boundary. See ADR-0005's second correction. | | `Elixir` | **1.20.4-otp-28** | ✅ `.tool-versions` | Confirmed at Slice 000: `elixir --version` reports Elixir 1.20.4 on Erlang/OTP 28, erts-16.4.0.5. Built-in type checker is part of the gate. `boundary` 0.10.4 compiles and enforces on this pair, measured at Slice 000 (H7). | -| `asdf` | v0.18.0 | 📐 `asdf --version` | `.tool-versions` committed in Slice 000. `mise` is absent on the build machine; measured at Slice 000 G1 with `which mise asdf`. asdf cannot pin itself, so this row is a command, not a file. ⚠️ Measured at Slice 001 line 3: asdf does **not** fail on a tool it has no plugin for — a `rust 1.92.0` line is omitted from `asdf current` and `asdf install` still exits 0. A pin file entry is only a pin where a plugin exists. | -| `Rust` | **1.92.0** | ✅ `rust-toolchain.toml` | Measured at Slice 001 line 3: `rustc --version` reports 1.92.0 (ded5c06cf 2025-12-08), exit 0. Pinned in `rust-toolchain.toml`, **not** `.tool-versions` — `asdf` here has no rust plugin and silently ignores a rust line, whereas `rustup show active-toolchain` reports this file as an override. See NOTES.md deviation D1. Corrected 2026-09-06: this row previously read `Rust + Tauri CLI | stable | ✅ .tool-versions`, which named a file carrying neither. | +| `asdf` | v0.18.0 | 📐 `asdf --version` | `.tool-versions` committed in Slice 000. `mise` is absent on the build machine; measured at Slice 000 G1 with `which mise asdf`. asdf cannot pin itself, so this row is a command, not a file. ⚠️ Measured at Slice 001 line 3: asdf does **not** fail on a tool it has no plugin for, a `rust 1.92.0` line is omitted from `asdf current` and `asdf install` still exits 0. A pin file entry is only a pin where a plugin exists. | +| `Rust` | **1.92.0** | ✅ `rust-toolchain.toml` | Measured at Slice 001 line 3: `rustc --version` reports 1.92.0 (ded5c06cf 2025-12-08), exit 0. Pinned in `rust-toolchain.toml`, **not** `.tool-versions`: `asdf` here has no rust plugin and silently ignores a rust line, whereas `rustup show active-toolchain` reports this file as an override. See NOTES.md deviation D1. Corrected 2026-09-06: this row previously read `Rust + Tauri CLI | stable | ✅ .tool-versions`, which named a file carrying neither. | | `Tauri CLI` | **2.11.4** | 📐 `_build/_tauri/bin/cargo-tauri tauri --version` | Measured at Slice 001 line 3. Not on `PATH` and not pinned by any file in the tree: `ex_tauri` provisions it with `cargo install tauri-cli --version ^2 --root .` inside `_build/_tauri`, which is gitignored, so `cargo tauri --version` exits 101 on a fresh machine. 📐 rather than ✅ because nothing at this sha verifies it. The `^2` floats; 2.11.4 is what it resolved to on 2026-09-06. | | `Zig` | **0.16.0** | ✅ `.tool-versions` | Measured at Slice 001 line 3: burrito 1.6.0 compares Zig for **equality**, not a range (`@zig_version_expected` in `deps/burrito/lib/burrito.ex`), and exits 1 on any other version. `zig version` reports 0.16.0, exit 0. Installed through the asdf zig plugin, added this slice. Corrected 2026-09-06: this row previously read `version required by Burrito | ✅ .tool-versions` and that file carried no zig line. | @@ -116,7 +116,7 @@ never pin a version hex marks as retired or vulnerable. | Name | Pin | Verified | Note | |---|---|---|---| -| `nx, exla` | latest stable | 🔍 not a single package | Local embeddings. EXLA binary size matters for desktop — measure in 032. Two packages, so no single lock key. | +| `nx, exla` | latest stable | 🔍 not a single package | Local embeddings. EXLA binary size matters for desktop: measure in 032. Two packages, so no single lock key. | | `bumblebee` | ~> 0.7 | 🔍 not yet a dependency | `all-MiniLM-L6-v2` embeddings; Whisper later. Added at Slice 032. | | `sqlite_vec` | ~> 0.1 | 🔍 not yet a dependency | Vectors in SQLite. Verify the loadable extension works inside the Burrito bundle (Slice 032). ⚠️ Pre-1.0, no release in roughly 22 months, 6,938 downloads all-time. R11's trigger already fires. Decide the fallback before Slice 032 starts. | | `hnswlib` | ~> 0.1.7 | 🔍 not yet a dependency | ⚠️ Pre-1.0. Optional accelerator; not on the critical path. | diff --git a/config/config.exs b/config/config.exs index 697044a..e59028d 100644 --- a/config/config.exs +++ b/config/config.exs @@ -13,7 +13,7 @@ import Config # when the generator put the same keys in this file. All environments, because # `ExTauri.ShutdownManager` runs in the packaged binary as well as in development. # -# `:version` is the **Tauri** version and only its major is consumed — +# `:version` is the **Tauri** version and only its major is consumed: # `ExTauri.Install.Helpers.extract_cli_version/1` takes the major and installs # `tauri-cli ^<major>`, which resolved to 2.11.4 on 2026-09-06 (VERSIONS.md carries that row # with its deriving command). 2.5.1 is ex_tauri's own suggested value, kept so this file does diff --git a/config/runtime.exs b/config/runtime.exs index c439d12..17c9c1e 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -81,7 +81,7 @@ if config_env() == :prod do # limit, not an oversight: sessions and signed cookies do not survive a restart of the # packaged app. Persisting a secret means writing a credential to the user's disk and # deciding its file mode, its rotation and what happens when it is copied to another machine - # — CLAUDE.md section 7 puts that in front of the owner, and slice 100 owns the desktop + # (CLAUDE.md section 7 puts that in front of the owner, and slice 100 owns the desktop # session story. A spike that quietly invented a credential store would be the larger sin. secret_key_base = System.get_env("SECRET_KEY_BASE") || Base.encode64(:crypto.strong_rand_bytes(48)) diff --git a/config/test.exs b/config/test.exs index f507024..0a568ae 100644 --- a/config/test.exs +++ b/config/test.exs @@ -19,7 +19,7 @@ config :trinity, Trinity.Repo, # `server: false` was the generator's default and it is right for controller tests, which go # through the plug pipeline without a socket. It is wrong for the one thing this slice has to # establish: `Trinity.Smoke` asks the endpoint which port it actually bound, and against a -# non-serving endpoint that question returns `{:error, :no_server_found}` — a red at an +# non-serving endpoint that question returns `{:error, :no_server_found}`: a red at an # earlier fault than the claim, which under CLAUDE.md section 8 demonstrates nothing. # # `port: 0` is the same ephemeral bind the packaged binary uses, so the test exercises the diff --git a/docs/00-vision.md b/docs/00-vision.md index 69e5487..d2c6dc6 100644 --- a/docs/00-vision.md +++ b/docs/00-vision.md @@ -1,8 +1,8 @@ -# 00 — Vision +# 00: Vision ## One sentence A personal AI agent that runs on your machine, remembers you, learns procedures, acts through tools, reaches -you on any surface, and never loses your work — built on the BEAM so those properties are structural, +you on any surface, and never loses your work, built on the BEAM so those properties are structural, not aspirational. ## What we are building @@ -30,7 +30,7 @@ substrate here. pieces that do so are built to be extractable as libraries in their own right. 10. **Donatable.** Trinity is built from commit 1 as an open-source project that can be proposed to the Agentic AI Foundation at Sandbox stage (ADR-0012): OSI license, governance files, DCO, SBOM, signed releases, a thesis. - It stays private until the owner says it is ready for the public. + It is developed in the open from the first commit. ## Non-goals (for v1) diff --git a/docs/01-architecture.md b/docs/01-architecture.md index 904b25f..a4f8718 100644 --- a/docs/01-architecture.md +++ b/docs/01-architecture.md @@ -1,4 +1,4 @@ -# 01 — Architecture +# 01: Architecture ## Shape @@ -71,7 +71,7 @@ without anything failing. | `Trinity.MCP` | Client manager, tool bridge, server | Tools, **Effects**, **Permissions**, Memory | | `Trinity.Gateways` | Adapter behaviour, router, allowlists, pairing | Sessions, **Permissions**, PubSub | | `Trinity.Subagents` | Delegation, result collection | Sessions, Tools | -| `Trinity.Sandbox` | Luerl runners, resource limits | — | +| `Trinity.Sandbox` | Luerl runners, resource limits | none | | `Trinity.Desktop` | ex_tauri bridge | PubSub | | `Trinity.Telemetry` | events, cost ledger, metrics | Repo | | `TrinityWeb` | LiveViews, components, API | all `Trinity.*` public APIs | diff --git a/docs/02-tech-stack.md b/docs/02-tech-stack.md index fa6749b..3995929 100644 --- a/docs/02-tech-stack.md +++ b/docs/02-tech-stack.md @@ -1,37 +1,37 @@ -# 02 — Tech stack +# 02: Tech stack Versions live in `VERSIONS.md`. This file explains *why* each choice was made and what would change it. | Concern | Choice | Why | What would change it | |---|---|---|---| | Runtime | Elixir 1.20 / OTP 28 | Latest Elixir with built-in type checking; OTP 28 is the newest ERTS Burrito precompiles | Burrito publishing OTP 29 ERTS → move to 29 (ADR-0005) | -| Web/UI | Phoenix 1.8 + LiveView 1.2 | Real-time streaming UI with server state; colocated hooks; scopes | — | -| HTTP server | Bandit | Pure Elixir, Phoenix default | — | +| Web/UI | Phoenix 1.8 + LiveView 1.2 | Real-time streaming UI with server state; colocated hooks; scopes | none | +| HTTP server | Bandit | Pure Elixir, Phoenix default | none | | DB (primary) | SQLite via ecto_sqlite3 | Zero-install desktop; FTS5 built in; sqlite_vec for vectors | Need for Oban Pro Workflows → Postgres (ADR-0002) | -| DB (secondary) | Postgres + pgvector | HNSW vectors, Oban Pro, multi-device server | — | -| Jobs/cron | Oban (Lite engine on SQLite) | Durable, retried, observable; free Oban Web | — | +| DB (secondary) | Postgres + pgvector | HNSW vectors, Oban Pro, multi-device server | none | +| Jobs/cron | Oban (Lite engine on SQLite) | Durable, retried, observable; free Oban Web | none | | LLM | req_llm | 20+ providers, streaming, tools, structured output, usage; Req-based; active | If it stalls, LangChain-Elixir is the fallback (same behaviour) | | MCP | decided by Slice 059 (fastest_mcp / gen_mcp / anubis_mcp / own server) | Target is 2026-07-28 with 2025-11-25 compat; anubis is ≤ 2025-11-25 and LGPL-3.0 | ADR-0007 finalised by measurement | | Embeddings | Bumblebee + EXLA (all-MiniLM-L6-v2) | Local, private, 384-dim | If EXLA binary size is unacceptable on desktop → hosted embeddings via req_llm, or Ortex (risk: stalled) | | Vector search | sqlite_vec (brute force) behind `VectorStore` behaviour | Fine to ~10^5 vectors; no extra process | Scale → hnswlib (pre-1.0) or pgvector HNSW | -| Shell tool | MuonTrap | Guaranteed child kill on process death; cgroups on Linux | — | +| Shell tool | MuonTrap | Guaranteed child kill on process death; cgroups on Linux | none | | Sandbox | Luerl (`sandbox` pkg) | In-VM, reduction-limited, no OS access | Untrusted native code → container/microVM (out of scope) | -| Modularity | `boundary` + behaviours + `Registry` | Compile-time enforcement of context deps | — | -| Config validation | nimble_options | Behaviour opts validated with docs generated | — | +| Modularity | `boundary` + behaviours + `Registry` | Compile-time enforcement of context deps | none | +| Config validation | nimble_options | Behaviour opts validated with docs generated | none | | Desktop shell | ex_tauri (Tauri 2 + Burrito sidecar) | Modern webview, tray, notifications, updater, signing plumbing; small footprint | Windows unsupported by ex_tauri → elixir-desktop or plain Tauri sidecar (Slice 001 decides; ADR-0004) | -| Packaging | Burrito | Single binary with ERTS | — | -| Gateways | Telegex (Telegram), Nostrum (Discord) | Active, supervised | — | +| Packaging | Burrito | Single binary with ERTS | none | +| Gateways | Telegex (Telegram), Nostrum (Discord) | Active, supervised | none | | Markdown streaming | phoenix_streamdown | LLM-optimised; freezes completed blocks | Verify in 013; fallback to earmark + chunk buffering | -| Testing | ExUnit, Mox, LiveViewTest (lazy_html) | Standard | — | -| Quality | credo, mix_audit, sobelow, ex_doc, Elixir type checker | Gate | — | +| Testing | ExUnit, Mox, LiveViewTest (lazy_html) | Standard | none | +| Quality | credo, mix_audit, sobelow, ex_doc, Elixir type checker | Gate | none | ## Explicitly not chosen (and why) -- **Umbrella apps** — isolation is enforced by `boundary` without the build/config overhead. -- **Jido** — *revised 2026-09-05:* reconsidered rather than rejected. Whether it expresses the action, directive and +- **Umbrella apps**: isolation is enforced by `boundary` without the build/config overhead. +- **Jido**: *revised 2026-09-05:* reconsidered rather than rejected. Whether it expresses the action, directive and effect layer better than plain OTP is ADR-0009, decided by measurement at the Slice 012 checkpoint. -- **Mnesia** — split-brain and schema-management sharp edges; SQLite/CubDB are simpler for single-node. -- **Ortex** — stalled since Nov 2024. Bumblebee/EXLA instead. -- **Electron** — heavier than Tauri; no advantage for a LiveView app. -- **LiveView Native** — mobile-oriented; not a desktop path. -- **Code.eval_string for skills** — unsafe by construction. Skills are data (SKILL.md) or Luerl. +- **Mnesia**: split-brain and schema-management sharp edges; SQLite/CubDB are simpler for single-node. +- **Ortex**: stalled since Nov 2024. Bumblebee/EXLA instead. +- **Electron**: heavier than Tauri; no advantage for a LiveView app. +- **LiveView Native**: mobile-oriented; not a desktop path. +- **Code.eval_string for skills**: unsafe by construction. Skills are data (SKILL.md) or Luerl. diff --git a/docs/03-conventions.md b/docs/03-conventions.md index ccbea21..8c0f0cc 100644 --- a/docs/03-conventions.md +++ b/docs/03-conventions.md @@ -1,4 +1,4 @@ -# 03 — Conventions +# 03: Conventions ## Code @@ -16,7 +16,7 @@ ## Tests - Unit tests for pure modules; process tests for GenServers/gen_statem (start under a test supervisor, send - messages, assert state via public API — not `:sys.get_state` except in recovery tests). + messages, assert state via public API, not `:sys.get_state` except in recovery tests). - LiveView tests use `Phoenix.LiveViewTest`; prefer `element/3` with text filters. - All external I/O behind behaviours, mocked with Mox. `test/support/mocks.ex` defines them. - Tagged tests: `@tag :live` (real providers, opt-in), `@tag :desktop` (needs Tauri), `@tag :slow`. @@ -33,8 +33,10 @@ - Branch per slice: `slice/NNN-short-name`. Delete after merge. - Conventional Commits with the slice id as scope: `feat(s022): …`, `fix(s022): …`, `test(s022): …`, `docs(s022): …`, `chore(s000): …`, `refactor(s012): …`. -- Final slice commit message: `feat(sNNN): complete slice NNN — <title>`. -- Merge: `git merge --no-ff`. Tag: `slice/NNN` (annotated). Never rebase or force-push `main`. +- Final slice commit message: `feat(sNNN): complete slice NNN (<title>)`. +- Merge: a pull request, merge-commit method only, `gate` green on the branch head (repository ruleset, no + bypass). Tag: `slice/NNN` (annotated), pushed after the merge; tags are protected against update and + deletion. Never rebase or force-push `main`; the ruleset refuses it anyway. - `mix.lock` is committed. Dependency changes are their own commit: `chore(sNNN): add req_llm ~> 1.10`. ## Definition of Done diff --git a/docs/04-slice-process.md b/docs/04-slice-process.md index f47fb7b..2d9fa23 100644 --- a/docs/04-slice-process.md +++ b/docs/04-slice-process.md @@ -1,4 +1,4 @@ -# 04 — Slice process +# 04: Slice process A slice is the unit of planning, work, proof, review, and history. It is small enough to review in one sitting and large enough to be worth a tag. diff --git a/docs/05-data-model.md b/docs/05-data-model.md index 3ffda84..ee8a29e 100644 --- a/docs/05-data-model.md +++ b/docs/05-data-model.md @@ -1,6 +1,6 @@ -# 05 — Data model +# 05: Data model -All tables have `id` (UUIDv7 as binary_id — sortable), `inserted_at`, `updated_at` (utc_datetime_usec). +All tables have `id` (UUIDv7 as binary_id, sortable), `inserted_at`, `updated_at` (utc_datetime_usec). SQLite is primary; every migration must also run on Postgres in the CI matrix. Use Ecto types that map on both (`:binary_id`, `:map` → JSON text on SQLite, `:utc_datetime_usec`). Vector columns and FTS tables are created with adapter-specific `execute/1` guarded by `repo().__adapter__()`. @@ -41,7 +41,7 @@ with adapter-specific `execute/1` guarded by `repo().__adapter__()`. | provider_meta | map | model, finish reason, latency | Append-only. Editing is a new message with `parts.supersedes`. -### messages_fts (Slice 031) — SQLite `fts5(content, session_id UNINDEXED, message_id UNINDEXED)`; on Postgres a +### messages_fts (Slice 031): SQLite `fts5(content, session_id UNINDEXED, message_id UNINDEXED)`; on Postgres a `tsvector` generated column on `messages`. ### memories (Slice 030/032) @@ -55,7 +55,7 @@ Append-only. Editing is a new message with `parts.supersedes`. | source_message_id | fk, nullable | provenance | | confidence | float | agent-assigned | | last_used_at | utc_datetime_usec | for decay/pruning | -Invariant: total bytes of `always_on` + `profile` for a persona ≤ configurable budget (default 8 KB) — enforced by +Invariant: total bytes of `always_on` + `profile` for a persona ≤ configurable budget (default 8 KB), enforced by `Trinity.Memory.Budget`, which triggers consolidation instead of silent truncation. ### skills (Slice 040) @@ -105,7 +105,7 @@ Per LLM call: `session_id`, `provider`, `model`, `prompt_tokens`, `completion_to `cost_usd`, `latency_ms`. Cost ledger and budgets derive from this. ### gateway_identities (Slice 070) -`adapter`, `external_user_id`, `display`, `paired_at`, `allowed` — DM pairing and allowlists. +`adapter`, `external_user_id`, `display`, `paired_at`, `allowed`: DM pairing and allowlists. ### receipts (Slice 024) | column | type | notes | diff --git a/docs/06-risk-register.md b/docs/06-risk-register.md index 6aea84b..c3fe09f 100644 --- a/docs/06-risk-register.md +++ b/docs/06-risk-register.md @@ -1,4 +1,4 @@ -# 06 — Risk register +# 06: Risk register | # | Risk | Likelihood | Impact | Trigger / early warning | Mitigation | Owner slice | |---|---|---|---|---|---|---| @@ -9,12 +9,12 @@ | R5 | req_llm breaking changes / provider drift | Med | Med | Live tests fail after bump | Behaviour isolates it; LangChain-Elixir fallback | 011 | | R6 | LiveView streaming performance (token spam) | Med | Low | UI lag at > 20 msg/s | Coalescing broadcaster; phoenix_streamdown | 013 | | R7 | Context compaction loses critical information | Med | High | Eval set regression | Always-on tier preserved verbatim; compaction eval harness in 023 | 023 | -| R8 | Prompt injection via tool output / skills / web | High | High | — | Untrusted-content framing; scanner on skills; permission gate on side effects; see 07-security-model | 021, 041 | +| R8 | Prompt injection via tool output / skills / web | High | High | none | Untrusted-content framing; scanner on skills; permission gate on side effects; see 07-security-model | 021, 041 | | R9 | Oban on SQLite limitations (no Pro workflows) | Low | Med | Need for multi-step durable graphs | Postgres path kept alive in CI matrix | 050 | | R10 | Agent scope creep across slices | Med | Med | PROOF shows work outside spec | CLAUDE.md rules; NOTES follow-ups; human review | all | -| R11 | Stale or single-maintainer libs (**boundary**, hnswlib, ex_tauri, sqlite_vec, nostrum, telegex) | High | Med–High | No release in 6 months. Measured 2026-09-05: boundary 2024-09-25, sqlite_vec 2024-11-19, telegex 1.9.0-rc.0 2024-09-18, nostrum 2025-03-02 — the trigger already fires for four of them | All behind behaviours; vendor if needed. **boundary is the highest-consequence one**: ADR-0001, docs/01, CLAUDE.md §5 and Slice 000 AC4 all rest on it, and it is unverified on Elixir 1.20. Probe it before anything is built on it | 000, 032, 072 | +| R11 | Stale or single-maintainer libs (**boundary**, hnswlib, ex_tauri, sqlite_vec, nostrum, telegex) | High | Med–High | No release in 6 months. Measured 2026-09-05: boundary 2024-09-25, sqlite_vec 2024-11-19, telegex 1.9.0-rc.0 2024-09-18, nostrum 2025-03-02, the trigger already fires for four of them | All behind behaviours; vendor if needed. **boundary is the highest-consequence one**: ADR-0001, docs/01, CLAUDE.md §5 and Slice 000 AC4 all rest on it, and it is unverified on Elixir 1.20. Probe it before anything is built on it | 000, 032, 072 | | R12 | Secrets leak into logs/DB/commits | Low | High | grep hits in CI | `mix gate` includes a secret scan (gitleaks-style regex) from 000; Secrets module from 100 | 000, 100 | -| R13 | Scope drifts toward matching other agents' breadth instead of shipping depth | Med | Low | Slice scope grows during a phase | Non-goals in docs/00 are binding; breadth is a later decision, not a default | — | +| R13 | Scope drifts toward matching other agents' breadth instead of shipping depth | Med | Low | Slice scope grows during a phase | Non-goals in docs/00 are binding; breadth is a later decision, not a default | none | | R14 | Elixir MCP libraries lag the 2026-07-28 spec; the one that claims it is weeks old | High | Med | 059 probes fail | Behaviour boundaries; own minimal stateless server as fallback; fastest_mcp/gen_mcp/anubis compared by measurement | 059 | | R15 | anubis_mcp is LGPL-3.0 | Med | Med | It wins the 059 spike | Legal review before adoption in a distributed binary; prefer Apache or MIT candidates | 059 | | R20 | Foundation donation may require transferring assets or marks the project intends to keep | Med | Med | Proposal drafting (122) | Unverified: the requirement is asserted from an announcement, not from the charter text. Read the charter, then decide what is offered and what is retained. Legal review before any proposal leaves the tree | 122 | diff --git a/docs/07-security-model.md b/docs/07-security-model.md index d98bcb4..b03b75b 100644 --- a/docs/07-security-model.md +++ b/docs/07-security-model.md @@ -1,4 +1,4 @@ -# 07 — Security model +# 07: Security model ## Trust boundaries @@ -61,7 +61,7 @@ Decisions are recorded (`approvals` table) and receipted. Approvals bind the can ## Sandbox (Slice 110) - Luerl with reduction limits, no `os`/`io`/`require`, no filesystem; explicit host functions only. -- Native/shell code is never "sandboxed" by the BEAM — the UI says so plainly when approving `:exec`. +- Native/shell code is never "sandboxed" by the BEAM: the UI says so plainly when approving `:exec`. ## Secrets @@ -83,4 +83,4 @@ Decisions are recorded (`approvals` table) and receipted. Approvals bind the can ## Data at rest -- SQLite file under the OS data dir with 0600 perms. Optional at-rest encryption is a later slice (SQLCipher via exqlite build flag) — noted, not planned. +- SQLite file under the OS data dir with 0600 perms. Optional at-rest encryption is a later slice (SQLCipher via exqlite build flag), noted rather than planned. diff --git a/docs/08-standards.md b/docs/08-standards.md index 3cf5f26..297e3b3 100644 --- a/docs/08-standards.md +++ b/docs/08-standards.md @@ -1,4 +1,4 @@ -# 08 — Standards landscape and Trinity's posture +# 08: Standards landscape and Trinity's posture Verified 2026-09-05. Re-verify at each phase boundary; this area moves monthly. @@ -29,20 +29,20 @@ Microsoft, OpenAI. Trinity targets AAIF-governed standards first; vendor-specifi |---|---|---|---|---| | **MCP 2026-07-28** | agent ↔ tools/data | Current stable. Date-versioned; there is no "2.0". Tier-1 SDKs went to major version 2 alongside it, which is where the nickname comes from; do not use it in any record. Tier-1 SDKs v2 shipped day-of. | **Primary target** for both client and server. Serve 2025-11-25 clients via compatibility for ≥ 12 months. | 059, 060, 061, 062 | | MCP 2025-11-25 | " | Legacy; 12-month deprecation policy applies to removed features | Client: connect to older servers. Server: compat profile. | 060, 061 | -| MCP 2024-11-05 | " | Obsolete | Not a target. Trinity speaks 2026-07-28 and serves 2025-11-25 for compatibility; anything older is out of scope. | — | -| MCP extensions: **Tasks**, **MCP Apps**, **Enterprise Managed Authorization (EMA)** | " | Official, versioned extensions. EMA = the IETF Identity Assertion JWT Authorization Grant (ID-JAG, "Cross-App Access"): the enterprise IdP mints an assertion via RFC 8693 token exchange; the MCP server's **own authorization server** redeems it via the RFC 7523 JWT-bearer grant and issues its access token. Adopted by MCP June 2026; vendors label it beta. | Tasks: yes. MCP Apps: later. **EMA: yes** — Trinity ships a small embedded AS so the enterprise-managed flow works without a third-party auth product (062). Identity is not authority: the gate, or the selected authority adapter, still decides whether an effect happens. | 061, 062 | +| MCP 2024-11-05 | " | Obsolete | Not a target. Trinity speaks 2026-07-28 and serves 2025-11-25 for compatibility; anything older is out of scope. | none | +| MCP extensions: **Tasks**, **MCP Apps**, **Enterprise Managed Authorization (EMA)** | " | Official, versioned extensions. EMA = the IETF Identity Assertion JWT Authorization Grant (ID-JAG, "Cross-App Access"): the enterprise IdP mints an assertion via RFC 8693 token exchange; the MCP server's **own authorization server** redeems it via the RFC 7523 JWT-bearer grant and issues its access token. Adopted by MCP June 2026; vendors label it beta. | Tasks: yes. MCP Apps: later. **EMA: yes**, Trinity ships a small embedded AS so the enterprise-managed flow works without a third-party auth product (062). Identity is not authority: the gate, or the selected authority adapter, still decides whether an effect happens. | 061, 062 | | MCP authorization (OAuth 2.1, RFC 9728 PRM, RFC 8707 resource indicators, RFC 8414/OIDC discovery, CIMD, RFC 9207) | auth | Normative in 2026-07-28; DCR deprecated → CIMD | Trinity's MCP server = OAuth 2.1 **resource server** for non-loopback clients; the authorization server is an owner decision, and it is an identity concern rather than an authority one (ADR-0008). Client = OAuth 2.1 client with PKCE + resource indicators; CIMD first, DCR fallback. | 060, 062 | | **Agent Skills** (agentskills.io, `SKILL.md`) | procedural knowledge | Open spec (Dec 2025), adopted by Claude Code, Codex, Gemini CLI, Copilot, Cursor, goose, Letta, 20+ others; `npx skills` distribution | Already ADR-0006. Add: honour `allowed-tools` frontmatter (experimental) as a permission hint; support `npx skills`-style GitHub install. | 040, 041 | | **AGENTS.md** | project context | AAIF founding project; simple markdown convention | Load `AGENTS.md` from the session's project root(s) into the context tier, with precedence and size cap. | 033 | | **A2A v1.0** | agent ↔ agent | Stable, Apache-2.0, LF/AAIF; Agent Cards, Tasks, streaming | Not v1. Optional later: publish an Agent Card and accept A2A tasks so other agents can delegate to Trinity; map A2A Task ↔ Trinity subagent. | 081 (optional) | -| ACP (Agent Client Protocol, Zed) | editor ↔ agent | Adopted by Zed, JetBrains and others | Not planned. Would let editors drive Trinity. Candidate follow-up after M6. | — | +| ACP (Agent Client Protocol, Zed) | editor ↔ agent | Adopted by Zed, JetBrains and others | Not planned. Would let editors drive Trinity. Candidate follow-up after M6. | none | | OpenTelemetry trace context in MCP `_meta` | observability | Documented convention in 2026-07-28 | Propagate `traceparent` through tool calls (fits Slice 090). | 090 | -## MCP 2026-07-28 — the changes that affect our design +## MCP 2026-07-28: the changes that affect our design 1. **Stateless core.** No `initialize`/`initialized`, no `Mcp-Session-Id`. Each request carries `io.modelcontextprotocol/protocolVersion` and `clientCapabilities` in `_meta`. Servers MUST implement - `server/discover`. Consequence: our MCP *server* is a stateless Plug — no per-connection process, trivially + `server/discover`. Consequence: our MCP *server* is a stateless Plug, no per-connection process, trivially embeddable in Phoenix and trivially runnable headless. Cross-call state must be explicit server-minted handles passed as tool arguments (we already have session ids). 2. **Multi Round-Trip Requests (MRTR)** replace server-initiated `sampling/createMessage`, `elicitation/create`, @@ -58,7 +58,7 @@ Microsoft, OpenAI. Trinity targets AAIF-governed standards first; vendor-specifi 5. **Subscriptions** via a single `subscriptions/listen` stream per opted-in notification type (`toolsListChanged`, …) instead of the GET endpoint. 6. **Cacheable lists**: `tools/list` etc. MUST return `ttlMs` + `cacheScope`; return tools in deterministic - order (prompt-cache friendly — we do this anyway). + order (prompt-cache friendly: we do this anyway). 7. **HTTP headers**: `Mcp-Method`, `Mcp-Name` required on POSTs; `x-mcp-header` passthrough. Useful for any gateway sitting in front of the server, which can route and authorize on headers without parsing bodies. 8. **Auth hardening** (see ADR-0008). @@ -72,6 +72,6 @@ Microsoft, OpenAI. Trinity targets AAIF-governed standards first; vendor-specifi | anubis_mcp 2.0.x | ≤ 2025-11-25 (no 2026-07-28 seen) | **LGPL-3.0** | Established (358k downloads), single maintainer | LGPL is a distribution consideration for a shipped desktop binary; needs legal review before adoption. | | fastest_mcp 0.3.x | **2026-07-28 + 2025-11-25**, client + server, OAuth, Tasks, MCP Apps, stdio + Streamable HTTP, OTel | Apache-2.0 | Very new (Aug 2026, ~400 downloads), Elixir ≥ 1.19 | Best feature match; adoption risk. | | gen_mcp 2.0.x | 2026-07-28 stateless server + compat plug for 2025 clients | verify | Server only | Would need a separate client. | -| Own implementation | — | — | — | The stateless server side is small (a Plug + JSON-RPC dispatch + `server/discover`). A viable fallback for the *server*; the *client* side (MRTR, tasks, OAuth) is more work. | +| Own implementation | none | none | none | The stateless server side is small (a Plug + JSON-RPC dispatch + `server/discover`). A viable fallback for the *server*; the *client* side (MRTR, tasks, OAuth) is more work. | Decision procedure: Slice 059 spike, then ADR-0007 is finalised. diff --git a/docs/packaging.md b/docs/packaging.md index 3248eda..eabacef 100644 --- a/docs/packaging.md +++ b/docs/packaging.md @@ -10,7 +10,7 @@ on the machine described under [Prerequisites](#prerequisites) on 2026-09-06; no quoted from a README. Where a thing was not measured, this file says so rather than estimating. The commands are `mix release` and Burrito. The Tauri shell is Slice 100's; what this file -covers is the half a command can answer — a single binary that boots, serves and stops. +covers is the half a command can answer: a single binary that boots, serves and stops. ## Prerequisites @@ -21,7 +21,7 @@ covers is the half a command can answer — a single binary that boots, serves a | Zig | **exactly** 0.16.0 | `.tool-versions` | Burrito 1.6.0 compares for equality, not a range, and exits 1 on anything else. Installed with the asdf zig plugin. | | Rust | 1.92.0 | `rust-toolchain.toml` | Only for the Tauri shell. **Not** `.tool-versions`: asdf here has no rust plugin and ignores such a line silently, so it would be a pin that pins nothing. `rustup` honours this file. | | Tauri CLI | 2.11.4 | nothing in the tree | `ex_tauri` installs it into `_build/_tauri`, which is gitignored. `cargo tauri --version` exits 101 on a fresh machine; the real command is `_build/_tauri/bin/cargo-tauri tauri --version`. | -| 7z or 7zz | any | — | **Windows target only.** Absent on the build machine, so the Windows target is unbuilt here. See [Targets](#targets). | +| 7z or 7zz | any | none | **Windows target only.** Absent on the build machine, so the Windows target is unbuilt here. See [Targets](#targets). | `VERSIONS.md`'s toolchain table carries the same figures with a mark that names where each one came from, and `mix versions.gen --check` fails the gate if the table and @@ -45,7 +45,7 @@ Three gotchas, all measured, all of which cost time before they were understood: a digested copy and a `.gz` sibling next to every static file; both are gitignored. 3. **`BURRITO_TARGET` selects one target** out of those declared in `mix.exs`. Without it, Burrito builds all three and the run fails on the first target whose host prerequisites are - missing — for this machine, Windows. + missing: for this machine, Windows. ## Run @@ -58,7 +58,7 @@ PHX_SERVER=true ./burrito_out/desktop_linux_x86_64 --no-halt # serves until `erl … -noshell -s elixir start_cli … -extra <argv>` (`deps/burrito/src/erlang_launcher.zig`). `elixir start_cli` is the ordinary Elixir CLI entry point, and it halts when its command list is empty, exactly as `elixir -e ''` does. Without -`--no-halt` the binary starts the endpoint and then exits immediately — and it exits **0**, so +`--no-halt` the binary starts the endpoint and then exits immediately, and it exits **0**, so nothing downstream notices. `--smoke` boots the app, asks the endpoint which port Bandit actually bound, prints @@ -66,7 +66,7 @@ nothing downstream notices. process. It is the only part of "the desktop app works" that a terminal can answer on a machine with no display. -## Measurements — linux x86_64 only +## Measurements: linux x86_64 only Machine: Linux 6.14.0-37-generic, x86_64. **These figures are for this host and this target.** Slice 001 AC6 asks for the same figures on macOS and Windows and they do not exist, because @@ -117,7 +117,7 @@ $ tr '\0' ' ' < /proc/2765267/cmdline /home/aylac/.local/share/.burrito/desktop_erts-16.4.0.5_0.1.0/erts-16.4.0.5/bin/beam.smp -- -root … ``` -That is the failure mode AC8 describes — closing the window should stop the sidecar — reached +That is the failure mode AC8 describes (closing the window should stop the sidecar), reached by signal rather than by window. The fix belongs in the wrapper or in a supervisor around it, neither of which is packaging wiring, so Slice 001 records it and Slice 100 owns it. @@ -127,14 +127,14 @@ Declared in `mix.exs`. Built here: one of three. | Target | Built on this machine | Built and run on a runner | Blocker here | |---|---|---|---| -| `linux_x86_64` | **yes**, 20 777 960 bytes | **yes** — 20 790 808 bytes, served HTTP 200 | — | -| `macos_aarch64` | **cross-compiles** only, 13 782 104 bytes, unsigned, never executed | **yes**, natively — 11 927 096 bytes, served HTTP 200 | Nothing here can execute a macOS binary. | -| `windows_x86_64` | **no** | **yes**, natively — 24 519 680 bytes, booted and exited under `--smoke`; not asked to serve | `** (RuntimeError) Couldn't find 7z/7zz` — the Windows ERTS ships as a `.exe` installer and Burrito unpacks it with 7z. None of `7z 7zz 7za 7zr` is installed and installing one needs root. | +| `linux_x86_64` | **yes**, 20 777 960 bytes | **yes**, 20 790 808 bytes, served HTTP 200 | none | +| `macos_aarch64` | **cross-compiles** only, 13 782 104 bytes, unsigned, never executed | **yes**, natively, 11 927 096 bytes, served HTTP 200 | Nothing here can execute a macOS binary. | +| `windows_x86_64` | **no** | **yes**, natively, 24 519 680 bytes, booted and exited under `--smoke`; not asked to serve | `** (RuntimeError) Couldn't find 7z/7zz`, the Windows ERTS ships as a `.exe` installer and Burrito unpacks it with 7z. None of `7z 7zz 7za 7zr` is installed and installing one needs root. | Runner evidence: `package` run `34067973983`, three jobs green. That `macos_aarch64` links under Zig here is a fact about the cross-compiler and **nothing about -whether the macOS app runs** — the runner is what established that it runs. The 11 927 096-byte +whether the macOS app runs**: the runner is what established that it runs. The 11 927 096-byte native build and the 13 782 104-byte cross build are different artifacts and are listed separately rather than averaged into one number. @@ -159,8 +159,8 @@ something it is not. **A runner cannot prove, and no job here will claim:** - that a **native window opens** on a real desktop. A GitHub runner has no desktop session; on - ubuntu-latest the job says which of two things it did — the shell under `xvfb-run`, or the - sidecar smoked alone with no display at all — and never leaves that ambiguous. + ubuntu-latest the job says which of two things it did: the shell under `xvfb-run`, or the + sidecar smoked alone with no display at all, and never leaves that ambiguous. - **first paint**, or anything else measured from pixels; - that **closing a window** stops the sidecar; - anything about **signing or notarisation**, which is Slice 101's. diff --git a/lib/mix/tasks/trinity.coverage.ex b/lib/mix/tasks/trinity.coverage.ex index e8a2471..b922595 100644 --- a/lib/mix/tasks/trinity.coverage.ex +++ b/lib/mix/tasks/trinity.coverage.ex @@ -4,8 +4,8 @@ defmodule Mix.Tasks.Trinity.Coverage do @shortdoc "Fails if line coverage dropped more than three points against the previous slice" @moduledoc """ - Reads `coverage.tsv` at the repo root — one row per slice, columns `slice_id`, `percent`, - `sha`, `date` — and compares the last two rows. + Reads `coverage.tsv` at the repo root: one row per slice, columns `slice_id`, `percent`, + `sha`, `date`, and compares the last two rows. `docs/03-conventions.md` sets the rule: a drop of more than three points fails until a `NOTES.md` justification names the reason. Slice 000 writes the first row, so it is the @@ -61,7 +61,7 @@ defmodule Mix.Tasks.Trinity.Coverage do case compare(prev, pct) do :ok -> - Mix.shell().info("trinity.coverage: #{id} #{pct}% vs #{prev_id} #{prev}% — OK") + Mix.shell().info("trinity.coverage: #{id} #{pct}% vs #{prev_id} #{prev}%: OK") {:error, drop} -> Mix.raise( diff --git a/lib/mix/tasks/trinity.names.ex b/lib/mix/tasks/trinity.names.ex index 4b477c2..76b3032 100644 --- a/lib/mix/tasks/trinity.names.ex +++ b/lib/mix/tasks/trinity.names.ex @@ -9,7 +9,7 @@ defmodule Mix.Tasks.Trinity.Names do ## Zero-permitted-site names Matched by **salted digest**, never by a plaintext pattern, because a pattern file spelling - them would itself be a hit — this module included. `priv/name_digests.txt` carries the salt + them would itself be a hit: this module included. `priv/name_digests.txt` carries the salt and the digests only; the generator that produces it lives outside this repository. Tokenisation, applied identically to file contents and to file paths: downcase, split on @@ -24,7 +24,7 @@ defmodule Mix.Tasks.Trinity.Names do The four platform names are matched in plain text and are allowed only inside one approved section of `README.md`. That section is located **by its heading text**, never by line - number — measured at slice 000, a generator run moved it from lines 46–62 to 72–88, and a + number: measured at slice 000, a generator run moved it from lines 46–62 to 72–88, and a hard-coded range would then have been reading the wrong sixteen lines. """ @@ -34,11 +34,11 @@ defmodule Mix.Tasks.Trinity.Names do @digests_path "priv/name_digests.txt" @permitted_file "README.md" @permitted_begin "## Connecting Trinity to the platform" - @permitted_end "## Principles baked into this plan" + @permitted_end "## Contributing, security and governance" @platform_names ~w(requisition ultraviolet sanction) # The plain-text set has to be spelled somewhere in order to be matched, so this module is - # the single path the PLATFORM-NAME scan skips — the same structural exemption enforcer 2 + # the single path the PLATFORM-NAME scan skips: the same structural exemption enforcer 2 # carries, and for the same reason. `test/trinity_names_test.exs` asserts it holds exactly # one entry so it cannot quietly grow. # diff --git a/lib/mix/tasks/trinity.reuse.ex b/lib/mix/tasks/trinity.reuse.ex index 1924b34..d17fd00 100644 --- a/lib/mix/tasks/trinity.reuse.ex +++ b/lib/mix/tasks/trinity.reuse.ex @@ -9,7 +9,7 @@ defmodule Mix.Tasks.Trinity.Reuse do **This check covers none of the name check.** They are separate rows in the gate and separate lines in PROOF.md; neither is ever reported as evidence for the other. - Files that cannot carry a comment — images, lockfiles, generated vendor assets — are covered + Files that cannot carry a comment (images, lockfiles, generated vendor assets) are covered by `REUSE.toml` instead and are listed there rather than being silently skipped here. """ @@ -43,9 +43,7 @@ defmodule Mix.Tasks.Trinity.Reuse do do: Mix.raise("REUSE.toml is missing (ADR-0012 decision 1).") if missing == [] do - Mix.shell().info( - "trinity.reuse: OK — every commentable tracked file carries an SPDX header" - ) + Mix.shell().info("trinity.reuse: OK. Every commentable tracked file carries an SPDX header") else Enum.each(missing, &Mix.shell().error("FAIL #{&1}: no #{@spdx}")) Mix.raise("trinity.reuse: #{length(missing)} file(s) without an SPDX header") diff --git a/lib/mix/tasks/versions.gen.ex b/lib/mix/tasks/versions.gen.ex index 6ecfce3..36f3360 100644 --- a/lib/mix/tasks/versions.gen.ex +++ b/lib/mix/tasks/versions.gen.ex @@ -4,7 +4,7 @@ defmodule Mix.Tasks.Versions.Gen do @shortdoc "Regenerates VERSIONS.md's tables from Trinity.Versions and mix.lock" @moduledoc """ - Writes the generated block in `VERSIONS.md` from `Trinity.Versions` — finding M6's one-way + Writes the generated block in `VERSIONS.md` from `Trinity.Versions`: finding M6's one-way data flow, so the prose cannot drift from the checked data. ## The verification mark is derived @@ -12,7 +12,7 @@ defmodule Mix.Tasks.Versions.Gen do A row is marked ✅ when its package is present in `mix.lock`, read through `Mix.Dep.Lock.read/0`, and 🔍 when it is absent. Toolchain rows are marked from `.tool-versions` instead, since they are not hex packages. **Nothing is marked by hand**, so - the mark cannot outlive the fact it asserts — which is what finding B3 caught twice. + the mark cannot outlive the fact it asserts, which is what finding B3 caught twice. `--check` regenerates in memory and fails if the file differs, which is how the gate asserts the tables match the data without parsing the markdown. @@ -57,10 +57,10 @@ defmodule Mix.Tasks.Versions.Gen do A `:toolchain` row is never a hex package, so it is marked from its own `:from`: - * `{:file, path, needle}` — ✅ naming that file when the file carries the pin, ❌ when it + * `{:file, path, needle}`: ✅ naming that file when the file carries the pin, ❌ when it does not. Before Slice 001 line 3 this clause was the constant `✅ \`.tool-versions\`` for every toolchain row, which marked Rust and Zig against a file carrying neither. - * `{:command, cmd}` — 📐 naming the command. Never ✅: nothing at this sha verifies it, and + * `{:command, cmd}`: 📐 naming the command. Never ✅: nothing at this sha verifies it, and a mark that says otherwise is the claim finding B3 caught. """ @spec mark(map(), :toolchain | :deps, MapSet.t(String.t())) :: String.t() diff --git a/lib/mix/tasks/versions.verify.ex b/lib/mix/tasks/versions.verify.ex index 9a2055f..a888751 100644 --- a/lib/mix/tasks/versions.verify.ex +++ b/lib/mix/tasks/versions.verify.ex @@ -13,13 +13,13 @@ defmodule Mix.Tasks.Versions.Verify do **Absence is not disagreement.** Most pinned packages arrive at a later slice and are marked 🔍 in `VERSIONS.md` until then; requiring them now would fail the gate for work nobody has - done. A pin that is not a version requirement at all — `not pinned`, `optional, ~> 0.3`, - `(transitive via LiveView test)` — is documentation, and there is nothing to satisfy. + done. A pin that is not a version requirement at all (`not pinned`, `optional, ~> 0.3`, + `(transitive via LiveView test)`) is documentation, and there is nothing to satisfy. It also reports any **direct dependency in `mix.exs` with no row in `Trinity.Versions`**, without which the pin list can silently fall behind the project it describes. - It does **not** parse `VERSIONS.md` — finding M6. That file is generated from the pin list. + It does **not** parse `VERSIONS.md`: finding M6. That file is generated from the pin list. """ use Boundary, classify_to: Trinity @@ -40,7 +40,7 @@ defmodule Mix.Tasks.Versions.Verify do if problems == [] do Mix.shell().info( - "versions.verify: OK — #{map_size(locked)} locked packages, none disagreeing with #{length(pins)} pins" + "versions.verify: OK. #{map_size(locked)} locked packages, none disagreeing with #{length(pins)} pins" ) else Enum.each(problems, &Mix.shell().error("FAIL #{&1}")) diff --git a/lib/trinity/application.ex b/lib/trinity/application.ex index e6b20c7..96bf06d 100644 --- a/lib/trinity/application.ex +++ b/lib/trinity/application.ex @@ -4,7 +4,7 @@ defmodule Trinity.Application do # The application supervises processes from both boundaries, so it is its own top-level # boundary rather than a member of Trinity. Without this, starting the endpoint reads as # Trinity depending on TrinityWeb, which docs/01 forbids. - # Trinity.Smoke is its own top-level boundary — it is the `--smoke` boot path and has to ask + # Trinity.Smoke is its own top-level boundary: it is the `--smoke` boot path and has to ask # TrinityWeb.Endpoint what port it bound, which Trinity (deps: []) may not do. Adding it here # is what lets the child list mention it. use Boundary, top_level?: true, deps: [Trinity, TrinityWeb, Trinity.Smoke], exports: [] diff --git a/lib/trinity/paths.ex b/lib/trinity/paths.ex index 56b3658..514241c 100644 --- a/lib/trinity/paths.ex +++ b/lib/trinity/paths.ex @@ -5,7 +5,7 @@ defmodule Trinity.Paths do Where the packaged app keeps its data on each OS. A Burrito binary is launched by a double-click with no environment prepared for it, so it - cannot ask for `DATABASE_PATH` the way `config/runtime.exs` does today — it has to work out + cannot ask for `DATABASE_PATH` the way `config/runtime.exs` does today: it has to work out where its own data lives. This module is that, and nothing else: no schema, no repo, no domain code. @@ -13,7 +13,7 @@ defmodule Trinity.Paths do `:os.type/0` and `System.get_env/1` are read once, in the arity-0 wrappers, and passed into the arity-2 functions. Every branch is therefore reachable from a test on this Linux machine - — which matters here, because the two branches that cannot be run on the owner's only machine + (which matters here, because the two branches that cannot be run on the owner's only machine are exactly the two that ship to users. `test/paths_test.exs` drives all three from a stub and asserts they do not collapse into one, which is how the first pass of this module failed. @@ -79,7 +79,7 @@ defmodule Trinity.Paths do # and joins a constant app name. An attacker who can set this process's environment has already # won by a shorter route than a path here. Scoped to this function rather than put in # .sobelow-skips because that file keys on file AND line, so any edit above this point silently - # reopens the finding — measured at slice 000 when SPDX headers moved router.ex:10 to :12. + # reopens the finding: measured at slice 000 when SPDX headers moved router.ex:10 to :12. @sobelow_skip ["Traversal.FileModule"] @spec ensure_data_dir() :: String.t() def ensure_data_dir do diff --git a/lib/trinity/smoke.ex b/lib/trinity/smoke.ex index 510faff..c0ea93c 100644 --- a/lib/trinity/smoke.ex +++ b/lib/trinity/smoke.ex @@ -76,7 +76,7 @@ defmodule Trinity.Smoke do It is a supervised child rather than a `Task.start/1` because CLAUDE.md section 5 says supervise everything and no bare spawn, and because running it inside `start/2` would halt - the VM from within the OTP boot sequence — a boot crash rather than a clean exit. `ps` + the VM from within the OTP boot sequence: a boot crash rather than a clean exit. `ps` cannot tell those apart from the outside; the exit code can, and AC7 reads both. """ @spec children([String.t()]) :: [Supervisor.child_spec() | {module(), term()}] diff --git a/lib/trinity/versions.ex b/lib/trinity/versions.ex index 3fd4370..af1d1b9 100644 --- a/lib/trinity/versions.ex +++ b/lib/trinity/versions.ex @@ -2,7 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 defmodule Trinity.Versions do @moduledoc """ - The machine-readable pin list — finding M6's single source of truth. + The machine-readable pin list: finding M6's single source of truth. `VERSIONS.md`'s tables are generated from this module by `mix versions.gen`, so the prose cannot drift from the checked data, and `mix versions.verify` compares it against `mix.lock`. @@ -16,7 +16,7 @@ defmodule Trinity.Versions do rows carry ✅ on `.tool-versions` and `elixir --version` instead, since they are not hex packages. - That replaces the old meaning — "someone ran `curl` against hex.pm on some date" — which is + That replaces the old meaning, "someone ran `curl` against hex.pm on some date", which is precisely the unverifiable claim finding B3 caught wrong twice, on the two packages the OTP pin rested on. @@ -29,10 +29,10 @@ defmodule Trinity.Versions do including Rust and Zig, which that file did not carry. Each toolchain row now states its `:from`: - * `{:file, path, needle}` — the pin lives in a file in the tree. `mix versions.gen` reads + * `{:file, path, needle}`: the pin lives in a file in the tree. `mix versions.gen` reads that file and marks the row from what it finds, so a row naming a file that stops carrying its pin fails the gate rather than keeping a stale ✅. - * `{:command, cmd}` — no file in the tree carries this pin, and only running `cmd` can + * `{:command, cmd}`: no file in the tree carries this pin, and only running `cmd` can answer. Marked 📐, never ✅, because nothing at this sha verifies it; the measurement lives in the slice's PROOF.md with its exit code. @@ -77,7 +77,7 @@ defmodule Trinity.Versions do lock: nil, from: {:command, "asdf --version"}, note: - "`.tool-versions` committed in Slice 000. `mise` is absent on the build machine; measured at Slice 000 G1 with `which mise asdf`. asdf cannot pin itself, so this row is a command, not a file. ⚠️ Measured at Slice 001 line 3: asdf does **not** fail on a tool it has no plugin for — a `rust 1.92.0` line is omitted from `asdf current` and `asdf install` still exits 0. A pin file entry is only a pin where a plugin exists." + "`.tool-versions` committed in Slice 000. `mise` is absent on the build machine; measured at Slice 000 G1 with `which mise asdf`. asdf cannot pin itself, so this row is a command, not a file. ⚠️ Measured at Slice 001 line 3: asdf does **not** fail on a tool it has no plugin for, a `rust 1.92.0` line is omitted from `asdf current` and `asdf install` still exits 0. A pin file entry is only a pin where a plugin exists." }, %{ name: "Rust", @@ -85,7 +85,7 @@ defmodule Trinity.Versions do lock: nil, from: {:file, "rust-toolchain.toml", "1.92.0"}, note: - "Measured at Slice 001 line 3: `rustc --version` reports 1.92.0 (ded5c06cf 2025-12-08), exit 0. Pinned in `rust-toolchain.toml`, **not** `.tool-versions` — `asdf` here has no rust plugin and silently ignores a rust line, whereas `rustup show active-toolchain` reports this file as an override. See NOTES.md deviation D1. Corrected 2026-09-06: this row previously read `Rust + Tauri CLI | stable | ✅ .tool-versions`, which named a file carrying neither." + "Measured at Slice 001 line 3: `rustc --version` reports 1.92.0 (ded5c06cf 2025-12-08), exit 0. Pinned in `rust-toolchain.toml`, **not** `.tool-versions`: `asdf` here has no rust plugin and silently ignores a rust line, whereas `rustup show active-toolchain` reports this file as an override. See NOTES.md deviation D1. Corrected 2026-09-06: this row previously read `Rust + Tauri CLI | stable | ✅ .tool-versions`, which named a file carrying neither." }, %{ name: "Tauri CLI", @@ -179,7 +179,7 @@ defmodule Trinity.Versions do pin: "latest stable", lock: nil, note: - "Local embeddings. EXLA binary size matters for desktop — measure in 032. Two packages, so no single lock key." + "Local embeddings. EXLA binary size matters for desktop: measure in 032. Two packages, so no single lock key." }, %{ name: "bumblebee", @@ -355,7 +355,7 @@ defmodule Trinity.Versions do def tables do [ %{ - title: "Toolchain — each row names its own pin file or command", + title: "Toolchain: each row names its own pin file or command", kind: :toolchain, rows: @toolchain }, diff --git a/mix.exs b/mix.exs index f4c18f2..af9b671 100644 --- a/mix.exs +++ b/mix.exs @@ -39,10 +39,10 @@ defmodule Trinity.MixProject do # the other two; nothing here claims they were built. # # Prerequisites measured at slice 001 line 3, both outside hex and both pinned: - # * Zig **exactly** 0.16.0 — burrito 1.6.0 compares for equality, not a range + # * Zig **exactly** 0.16.0: burrito 1.6.0 compares for equality, not a range # (deps/burrito/lib/burrito.ex `@zig_version_expected`). Pinned in `.tool-versions`. # * Rust 1.92.0 for the Tauri shell. Pinned in `rust-toolchain.toml`, not `.tool-versions` - # — see NOTES.md deviation D1. + # (see NOTES.md deviation D1). defp releases do [ desktop: [ @@ -129,8 +129,8 @@ defmodule Trinity.MixProject do {:ex_doc, "~> 0.38", only: :dev, runtime: false}, {:nimble_options, "~> 1.1"}, # Slice 001 line 1, arm (a) recorded `only: :dev`. **Corrected at G4**, and the reason is - # the shell, not the tooling: `ExTauri.ShutdownManager` is the sidecar's heartbeat — the - # Rust window's only way to tell the BEAM it has closed — so it has to exist in the + # the shell, not the tooling: `ExTauri.ShutdownManager` is the sidecar's heartbeat, the + # Rust window's only way to tell the BEAM it has closed, so it has to exist in the # binary that ships, and a `:dev`-only dependency does not. `mix ex_tauri.install` adds # that child unconditionally, which is why the generator's output could not start under # MIX_ENV=test or MIX_ENV=prod. Recorded as deviation D7 in NOTES.md. @@ -200,7 +200,7 @@ defmodule Trinity.MixProject do # The plan's own consistency, as the gate's final step rather than a second command # with a second exit code. Added at slice 001 G4, for a mistake made three times in # this slice: `mix gate` and `scripts/plan_check.sh` were run as a pair, the gate's - # `exit=0` was read, and `plan_check exit=1` on the line below it was not — twice + # `exit=0` was read, and `plan_check exit=1` on the line below it was not: twice # reaching the remote. Two results printed and one read is a reporting failure the # tooling can remove, so it is removed: **one command, one exit code.** # diff --git a/mix.lock b/mix.lock index 7393bc0..759a76f 100644 --- a/mix.lock +++ b/mix.lock @@ -27,14 +27,14 @@ "glob_ex": {:hex, :glob_ex, "0.1.12", "7b2d9369c20e2697efcfd185d13d6e84c94cd3bfd2730fbde613141c2e015c00", [:mix], [], "hexpm", "2e2fac83f113514434c7eaf267b4c38af2f91766f1cab2c5db7053b7fc1ee0bb"}, "heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "0435d4ca364a608cc75e2f8683d374e55abbae26", [tag: "v2.2.0", sparse: "optimized", depth: 1]}, "hpax": {:hex, :hpax, "1.0.4", "777de5d433b0fbdc7c418159c8055910faa8047ffdb3d6b31098d2a46cd7685c", [:mix], [], "hexpm", "afc7cb142ebcc2d01ce7816190b98ce5dd49e799111b24249f3443d730f377ca"}, - "igniter": {:hex, :igniter, "0.8.3", "9de74d3885efae43b0b58dc6f7b816963c4bbd391e6b6fe6922ee21c4e384c76", [:mix], [{:ex_ast, "~> 0.5", [hex: :ex_ast, repo: "hexpm", optional: false]}, {:glob_ex, "~> 0.1.7", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:jason, "~> 1.4.5", [hex: :jason, repo: "hexpm", optional: false]}, {:owl, "~> 0.11", [hex: :owl, repo: "hexpm", optional: false]}, {:phx_new, "~> 1.7", [hex: :phx_new, repo: "hexpm", optional: true]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}, {:rewrite, ">= 1.1.1 and < 2.0.0-0", [hex: :rewrite, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.4", [hex: :sourceror, repo: "hexpm", optional: false]}, {:spitfire, ">= 0.1.3 and < 1.0.0-0", [hex: :spitfire, repo: "hexpm", optional: false]}], "hexpm", "afc5e3848d885e680da5c3b65e5e7717555a08cd12305190ff2be76427af39ff"}, + "igniter": {:hex, :igniter, "0.8.4", "f79f1bbdc2fb7b9ca030a22d12a585b060cbf5b94b9d3f23b1148578a9e05d11", [:mix], [{:ex_ast, "~> 0.5", [hex: :ex_ast, repo: "hexpm", optional: false]}, {:glob_ex, "~> 0.1.7", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:jason, "~> 1.4.5", [hex: :jason, repo: "hexpm", optional: false]}, {:owl, "~> 0.11", [hex: :owl, repo: "hexpm", optional: false]}, {:phx_new, "~> 1.7", [hex: :phx_new, repo: "hexpm", optional: true]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}, {:rewrite, ">= 1.1.1 and < 2.0.0-0", [hex: :rewrite, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.4", [hex: :sourceror, repo: "hexpm", optional: false]}, {:spitfire, ">= 0.1.3 and < 1.0.0-0", [hex: :spitfire, repo: "hexpm", optional: false]}], "hexpm", "a9b1cbec996ccb100b4f7d8130129b2dd3f18eb4224ac9a0e907e428ca90dbd7"}, "jason": {:hex, :jason, "1.4.5", "2e3a008590b0b8d7388c20293e9dcc9cf3e5d642fd2a114e4cbbb52e595d940a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b0c823996102bcd0239b3c2444eb00409b72f6a140c1950bc8b457d836b30684"}, "lazy_html": {:hex, :lazy_html, "0.1.12", "31a55ee622918fce988c94b06232227b42daa64e4eab14ac32081d0f3fd8db6f", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.9", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:fine, "~> 0.1.0", [hex: :fine, repo: "hexpm", optional: false]}], "hexpm", "8a0da594776caee58782c6f93b2abaa5bdb809daf8d43351a561f7de9dc2e2a8"}, "makeup": {:hex, :makeup, "1.2.2", "882d46dc0905e9ff7abf2aab61a7e6b3dcc555533977d8a23b06019e6c89ac94", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "9a1a24e5b343b8ae16abea0822c10a6f75da27af7fa802ada5251f7579bfccfa"}, "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, "makeup_erlang": {:hex, :makeup_erlang, "1.1.0", "835f7e60792e08824cda445639555d7bf1bbbddb1b60b306e33cb6f6db24dc74", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "1cd6780fb1dd1a03979abaed0fe82712b0625118fd5257d3ebbf73f960c73c3c"}, "mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"}, - "mint": {:hex, :mint, "1.10.0", "85af3353bfc504f5bdfe494bd92b8490f87a306dc659ee1ad0af435107e898dc", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "8b16fb72aaa7531d206a1f05e4cc85509ba531ccec7a17a22736c9c95cbb24d1"}, + "mint": {:hex, :mint, "1.10.1", "c53e70867cf74017716884d8d33e0742b08b32e9cdb0031cbc69a429dc5555e3", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "0ba2a904605ed8406393444fb8b3356dc58eb59ee6c7fb94ac3f015e1be129e8"}, "mix_audit": {:hex, :mix_audit, "2.1.5", "c0f77cee6b4ef9d97e37772359a187a166c7a1e0e08b50edf5bf6959dfe5a016", [:make, :mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.11", [hex: :yaml_elixir, repo: "hexpm", optional: false]}], "hexpm", "87f9298e21da32f697af535475860dc1d3617a010e0b418d2ec6142bc8b42d69"}, "mox": {:hex, :mox, "1.3.1", "ccd9ddeacc1eb1e4fe9ac42f99fcb49b214e3529b8c3b1bd7a60bc803a46f536", [:mix], [{:nimble_ownership, "~> 1.0", [hex: :nimble_ownership, repo: "hexpm", optional: false]}], "hexpm", "6aa44b17e40abed6c6d501e6393d229f2820fb69c5971fd17fe0d7f7eefa41fd"}, "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, @@ -55,8 +55,8 @@ "req": {:hex, :req, "0.7.4", "23e9ffec17de032a46a4b15ed65c09793893bf4a7c680f4bbf6227fce6bdf74d", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:finch, "~> 0.21", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "4b192d63253e8dcc6221ef992ea9ebef7d3555166e8423aa5b553e86bc3c69a2"}, "rewrite": {:hex, :rewrite, "1.3.0", "67448ba7975690b35ba7e7f35717efcce317dbd5963cb0577aa7325c1923121a", [:mix], [{:glob_ex, "~> 0.1", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.0", [hex: :sourceror, repo: "hexpm", optional: false]}, {:text_diff, "~> 0.1", [hex: :text_diff, repo: "hexpm", optional: false]}], "hexpm", "d111ac7ff3a58a802ef4f193bbd1831e00a9c57b33276e5068e8390a212714a5"}, "sobelow": {:hex, :sobelow, "0.15.0", "b067d7f8522a9d758fa89cb2bfcbab7ad72c45a0993cb958c989c6fd956fdd56", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "24a800e2d7fa8c3bd21561b6ad8ad4745ed726a09fd606598981d9048708da98"}, - "sourceror": {:hex, :sourceror, "1.12.2", "85bfd48159f020c0cbfc72f289f11456fdc05dc43719b6f2589fb969faefa113", [:mix], [], "hexpm", "da37d3da09c5b890528802c7056a8f585a061973820d7656b6e3649c14f0e9cb"}, - "spitfire": {:hex, :spitfire, "0.4.1", "69e90335d00ca328295e1e1e77cac5d7575aa6d34274e3467ebfc654b8858be3", [:mix], [], "hexpm", "27d86f67681179682b15c6758d64ac2eb2b3637ed8340800c8b885c69754cdcd"}, + "sourceror": {:hex, :sourceror, "1.12.3", "f58eebef0765c7a369a49a755ab2a5ee88d92777f403ea5a08b722cefcc37f51", [:mix], [], "hexpm", "d5f2f37099de794840f08c54ae546d7f6e4ea015e397be64aebe4996fa9f7da7"}, + "spitfire": {:hex, :spitfire, "0.4.2", "5c719208d4eeb810e5b2a2aa1024d1e4b2974b7ca422c274b2486666e5159735", [:mix], [], "hexpm", "9bbbbffe93e6f88ccf193487ef56b83c2646a6dc3875bb0975d4654bfe96c5bb"}, "tailwind": {:hex, :tailwind, "0.5.1", "35435b13158c90d37da11e1cfc808755fca1d7b6c5ab87b1b19c5de87e2f0a10", [:mix], [], "hexpm", "c4e26302a59fec72abc5610ecb6ad2116d9aa31f31aab2d4b8eb6e95d25a689c"}, "telemetry": {:hex, :telemetry, "1.4.2", "a0cb522801dffb1c49fe6e30561badffc7b6d0e180db1300df759faa22062855", [:rebar3], [], "hexpm", "928f6495066506077862c0d1646609eed891a4326bee3126ba54b60af61febb1"}, "telemetry_metrics": {:hex, :telemetry_metrics, "1.2.0", "7632c19c01d88d8aaca5da1a0e8912f5af39b79e7c08a2c253aeb3c14c2c957e", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "71dde12fc29b58b9c77ec17ec319109e5ca848d010fc1965ed4463bba1837c07"}, diff --git a/rust-toolchain.toml b/rust-toolchain.toml index f068807..d0c456b 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,7 +1,7 @@ # The Rust pin for the Tauri shell. # # This is not `.tool-versions`. Measured at slice 001 line 3: `asdf` on this machine has -# plugins for elixir, erlang and zig, and none for rust — and asdf does not fail on a tool it +# plugins for elixir, erlang and zig, and none for rust, and asdf does not fail on a tool it # has no plugin for, it omits the line from `asdf current` and exits 0. A `rust 1.92.0` line # there would be a pin that pins nothing. `rustup` owns Rust here and honours this file, which # `rustup show active-toolchain` reports as an override. See NOTES.md, deviation D1. diff --git a/scripts/plan_check.sh b/scripts/plan_check.sh index 8932dba..b0a15d7 100755 --- a/scripts/plan_check.sh +++ b/scripts/plan_check.sh @@ -1,12 +1,12 @@ #!/usr/bin/env bash # SPDX-FileCopyrightText: Sudo Apt Holdings LLC # SPDX-License-Identifier: Apache-2.0 -# plan_check.sh — enforces the plan-consistency rules that were previously prose. +# plan_check.sh: enforces the plan-consistency rules that were previously prose. # # Every rule below was a finding that closed on a hand check and stayed broken. # Populations derive from `git ls-files`; nothing here is a hand list. # -# Scope note for check 6: it validates references to PLAN artifacts only — +# Scope note for check 6: it validates references to PLAN artifacts only: # docs/, docs/adr/, slices/, templates/ and the root records. Paths under # lib/, test/, priv/, config/, .github/ and tauri/ are deliberately excluded: # they name code this plan has not built yet, so their absence is expected and @@ -124,7 +124,7 @@ if git grep -nIE '\bSCR-[0-9]+\b' -- '*.md' 'scripts/*' >/dev/null 2>&1; then fail=1 fi -section "8. Commit messages: no assistant attribution, every commit signed off" +section "8. Commit messages: no attribution trailers, every commit signed off" # Checks the history, not the hook. A bypassed or unconfigured hook still fails here. for c in $(git log --format=%H); do body=$(git log -1 --format=%B "$c") @@ -184,7 +184,7 @@ case "$branch" in st=$(roadmap_status "$sid") case "$st" in # `approved` added at slice 001 G4. The lifecycle is ready -> in_progress -> done -> - # approved, and docs/04 has the owner set `approved` at G4 — which happens while the + # approved, and docs/04 has the owner set `approved` at G4, which happens while the # slice branch still exists, because CLAUDE.md section 4 puts the status change in the # branch's own commit and the merge comes after. Without this the landing commit # cannot pass its own gate, which is where it was found. diff --git a/slices/010-core-domain-persistence/SLICE.md b/slices/010-core-domain-persistence/SLICE.md index 63f161f..4a3fd84 100644 --- a/slices/010-core-domain-persistence/SLICE.md +++ b/slices/010-core-domain-persistence/SLICE.md @@ -1,4 +1,4 @@ -# Slice 010 — Core domain + persistence +# Slice 010: Core domain + persistence | Field | Value | |---|---| @@ -24,7 +24,7 @@ Data outlives processes. Everything later rehydrates from these tables. data dir. Acquire an advisory lock or a lockfile carrying pid and mode at boot, and refuse to start with a named reason when it is held. - `TRINITY_DB=postgres` config branch (`postgrex`); CI matrix job with a Postgres service. -- Migrations: `personas` (minimal: name, soul, model — full use in 030), `sessions`, `messages` per `docs/05-data-model.md`. +- Migrations: `personas` (minimal: name, soul, model; full use in 030), `sessions`, `messages` per `docs/05-data-model.md`. - `Trinity.Sessions` persistence functions: `create_session/1`, `get_session/1`, `list_sessions/1`, `append_message/2` (assigns `seq` atomically), `history/2` (ordered, with limit/offset), `archive/1`. - `Trinity.Sessions.Store` internal module isolates queries; `boundary` `exports: [Trinity.Sessions]`. @@ -38,7 +38,7 @@ Data outlives processes. Everything later rehydrates from these tables. ## Design notes - `append_message/2` must be a single transaction that reads max(seq) and inserts; on SQLite with one writer this is race-free; on Postgres use `SELECT … FOR UPDATE` on the session row. Test both. -- Keep `parts` as `:map` — SQLite stores JSON text; Postgres jsonb. +- Keep `parts` as `:map`: SQLite stores JSON text; Postgres jsonb. ## Deliverables - `config/*.exs` DB branches, `priv/repo/migrations/*`, `lib/trinity/sessions/{session,message,store}.ex`, `lib/trinity/sessions.ex`, `lib/trinity/paths.ex` (if not from 001), tests, CI matrix update, `docs/05-data-model.md` synced. @@ -48,7 +48,7 @@ Data outlives processes. Everything later rehydrates from these tables. 2. [auto] Property/stress test passes on both adapters: gapless `seq` per session under concurrency; `integrity_check` ok. 3. [auto] `append_message/2` rejects unknown roles and empty content with `{:error, %Ecto.Changeset{}}`. 4. [auto] `history/2` returns messages in `seq` order and respects `limit`. -5. [auto] `boundary` prevents `TrinityWeb` from calling `Trinity.Sessions.Store` directly (test compiles a violating module in a tmp dir — or document the compile error). +5. [auto] `boundary` prevents `TrinityWeb` from calling `Trinity.Sessions.Store` directly (test compiles a violating module in a tmp dir, or document the compile error). 6. [auto] A second instance started against a held data dir refuses to start, names the holder's pid and mode, and leaves the database untouched (test). 7. [auto] Gate green; coverage line reported. @@ -63,7 +63,7 @@ If that changes during the slice, the criterion is retagged and this section is - [ ] gate green · [ ] AC1–7 proven · [ ] docs/05 updated · [ ] ROADMAP → done · [ ] commit + tag ## Commit & tag -`feat(s010): complete slice 010 — core domain and persistence` · tag `slice/010` +`feat(s010): complete slice 010 (core domain and persistence)` · tag `slice/010` ## Risks / open questions - `ecto_sqlite3` and Oban Lite both want the same file; confirm pool settings when Oban arrives (050). diff --git a/slices/011-llm-provider-layer/SLICE.md b/slices/011-llm-provider-layer/SLICE.md index d6a17b0..9bbfa3d 100644 --- a/slices/011-llm-provider-layer/SLICE.md +++ b/slices/011-llm-provider-layer/SLICE.md @@ -1,4 +1,4 @@ -# Slice 011 — LLM provider layer +# Slice 011: LLM provider layer | Field | Value | |---|---| @@ -52,14 +52,14 @@ Provider-agnostic by construction (Vision goal 6). Sessions never see a vendor S ## Manual verification queue Every `[manual]` criterion below needs a person. Listed here so the owner sees the queue at G1 rather than at review time. -- **AC2** — `generate_object/3` returns a validated map for a given JSON schema (fake) and (live-tagged) for one real provider. -- **AC6** — `mix test --only live` passes against at least one configured provider on the developer machine (output pasted; keys redacted). +- **AC2**: `generate_object/3` returns a validated map for a given JSON schema (fake) and (live-tagged) for one real provider. +- **AC6**: `mix test --only live` passes against at least one configured provider on the developer machine (output pasted; keys redacted). ## Definition of Done - [ ] gate green · [ ] AC1–7 proven · [ ] VERSIONS updated · [ ] ROADMAP → done · [ ] commit + tag ## Commit & tag -`feat(s011): complete slice 011 — LLM provider layer` · tag `slice/011` +`feat(s011): complete slice 011 (LLM provider layer)` · tag `slice/011` ## Risks / open questions -- req_llm event shapes may differ per provider; the normalisation layer is the contract — test it per provider in live tests. +- req_llm event shapes may differ per provider; the normalisation layer is the contract: test it per provider in live tests. diff --git a/slices/012-session-process-agent-loop/SLICE.md b/slices/012-session-process-agent-loop/SLICE.md index 61f6989..9f25c21 100644 --- a/slices/012-session-process-agent-loop/SLICE.md +++ b/slices/012-session-process-agent-loop/SLICE.md @@ -1,4 +1,4 @@ -# Slice 012 — Session process + agent loop +# Slice 012: Session process + agent loop | Field | Value | |---|---| @@ -21,7 +21,7 @@ Vision goals 1 and 2. Silent process death and the one-agent-per-machine limitat - `Trinity.Sessions.Session` gen_statem with states: `idle`, `thinking`, `tool_wait`, `approval_wait` (stub), `compacting` (stub), `error`. - `Trinity.Sessions.start_session/1`, `ensure_started/1` (idempotent, rehydrates), `send_user_message/2`, `cancel_turn/1`, `state/1`, `subscribe/1`. - Turn pipeline: `Trinity.Sessions.Prompt.build/2` (system = persona stub + memory stub + history) → `Trinity.LLM.stream_to/3` in a Task under the session's `Task.Supervisor` → event handling → persist assistant message (parts + usage) → broadcast → idle. -- Tool-call handling: parse tool calls into pending list; execution is a stub behaviour `Trinity.Sessions.ToolRunner` that 020 replaces (returns `{:error, :no_tools}` now) — the state machine path must be complete. +- Tool-call handling: parse tool calls into pending list; execution is a stub behaviour `Trinity.Sessions.ToolRunner` that 020 replaces (returns `{:error, :no_tools}` now): the state machine path must be complete. - Persistence-before-broadcast rule; partial assistant text persisted every N chunks or M ms as a draft message (`parts.draft: true`), finalised at done. - Rehydrate: on init, load session + history; if a draft exists, mark it interrupted and broadcast `{:turn_interrupted, ...}`; do not auto-resume (a resume policy is a later slice). - Idle timeout: hibernate after X min; stop after Y (config); `ensure_started/1` restarts on demand. @@ -62,7 +62,7 @@ If that changes during the slice, the criterion is retagged and this section is - [ ] gate green · [ ] AC1–9 proven · [ ] docs/01 tree updated · [ ] ROADMAP → done · [ ] commit + tag ## Commit & tag -`feat(s012): complete slice 012 — session process and agent loop` · tag `slice/012` +`feat(s012): complete slice 012 (session process and agent loop)` · tag `slice/012` ## Risks / open questions - Draft-persistence write frequency vs SQLite single writer: measure; default every 500 ms or 2 KB. diff --git a/slices/013-liveview-chat-streaming/SLICE.md b/slices/013-liveview-chat-streaming/SLICE.md index 037852f..5315475 100644 --- a/slices/013-liveview-chat-streaming/SLICE.md +++ b/slices/013-liveview-chat-streaming/SLICE.md @@ -1,4 +1,4 @@ -# Slice 013 — LiveView chat UI with streaming +# Slice 013: LiveView chat UI with streaming | Field | Value | |---|---| @@ -42,13 +42,13 @@ M1 "Talks" is only real if a human can use it. Also the surface where every late - `lib/trinity_web/live/session_live/*`, components, router, assets (hooks), tests, `docs/` screenshots in `proof/`. ## Acceptance criteria -1. [manual] Manual: create session, send "hello", see streamed markdown response (FakeProvider in dev via config flag, and a real provider) — screenshot/GIF. +1. [manual] Manual: create session, send "hello", see streamed markdown response (FakeProvider in dev via config flag, and a real provider), screenshot/GIF. 2. [auto] LiveView test: send → `assistant_delta` updates → final message appears once in the DOM (no duplication). 3. [manual] Cancel during streaming: button works; interrupted message rendered with banner (test + screenshot). 4. [manual] Kill the Session process while the page is open: banner appears; page remains usable; next message works (manual + test using `Process.exit`). 5. [auto] Reload the page mid-stream: history renders from DB; no duplicate or missing messages (test). 6. [auto] Model picker changes `sessions.model` and the next turn uses it (test with FakeProvider recording the model). -7. [auto] Render performance: 1,000 deltas in 1 s do not exceed ~25 DOM patches (count via `phx-update` hooks or telemetry) — number recorded. +7. [auto] Render performance: 1,000 deltas in 1 s do not exceed ~25 DOM patches (count via `phx-update` hooks or telemetry): number recorded. 8. [auto] Gate green; `mix sobelow` no new findings. ## Proof required @@ -57,15 +57,15 @@ M1 "Talks" is only real if a human can use it. Also the surface where every late ## Manual verification queue Every `[manual]` criterion below needs a person. Listed here so the owner sees the queue at G1 rather than at review time. -- **AC1** — Manual: create session, send "hello", see streamed markdown response (FakeProvider in dev via config flag, and a real provider) — screenshot/GIF. -- **AC3** — Cancel during streaming: button works; interrupted message rendered with banner (test + screenshot). -- **AC4** — Kill the Session process while the page is open: banner appears; page remains usable; next message works (manual + test using `Process.exit`). +- **AC1**: Manual: create session, send "hello", see streamed markdown response (FakeProvider in dev via config flag, and a real provider), screenshot/GIF. +- **AC3**: Cancel during streaming: button works; interrupted message rendered with banner (test + screenshot). +- **AC4**: Kill the Session process while the page is open: banner appears; page remains usable; next message works (manual + test using `Process.exit`). ## Definition of Done - [ ] gate green · [ ] AC1–8 proven · [ ] VERSIONS (phoenix_streamdown ✅ or fallback noted) · [ ] ROADMAP → done · [ ] commit + tag ## Commit & tag -`feat(s013): complete slice 013 — LiveView chat UI with streaming` · tag `slice/013` +`feat(s013): complete slice 013 (LiveView chat UI with streaming)` · tag `slice/013` ## Risks / open questions - The first-candidate renderer is a pre-release four months without a release. Treat the `earmark`/`mdex` chunk-buffering fallback as a live option, not a formality, and record the measurement and the choice in NOTES + VERSIONS. diff --git a/slices/020-tool-protocol-registry/SLICE.md b/slices/020-tool-protocol-registry/SLICE.md index a0b5b2a..868bcd7 100644 --- a/slices/020-tool-protocol-registry/SLICE.md +++ b/slices/020-tool-protocol-registry/SLICE.md @@ -1,4 +1,4 @@ -# Slice 020 — Tool protocol + registry +# Slice 020: Tool protocol + registry | Field | Value | |---|---| @@ -10,7 +10,7 @@ ## Goal The `Trinity.Tools.Tool` behaviour, a registry that discovers tools from config (and later from MCP and skills), JSON-schema generation for the LLM, and the real `Trinity.Sessions.ToolRunner` that executes tool calls in -supervised Tasks with timeouts, parallelism, and structured results. No side-effecting tools yet — one `echo` +supervised Tasks with timeouts, parallelism, and structured results. No side-effecting tools yet: one `echo` tool and one deliberately crashing tool for tests. ## Why @@ -24,7 +24,7 @@ Modularity promise: "adding a tool is adding a module". Everything in phases 2, - Toolsets: config groups (`:core`, `:web`, `:shell`…) that personas/sessions enable. - ToolRunner: executes N tool calls from one assistant turn concurrently via `Task.Supervisor.async_stream_nolink` with per-tool timeout; each result becomes a `tool` message; crash → error result, session continues. - Argument validation against `schema/0` before execute; invalid → error result the model can read. -- Permission hook point: `Trinity.Permissions.decide/3` called before execute — stub returns `:allow` (real in 021). +- Permission hook point: `Trinity.Permissions.decide/3` called before execute: stub returns `:allow` (real in 021). - Test tools in `test/support/tools/`: `Echo`, `Sleep`, `Crash`, `Big` (returns > cap). **Out:** - Real tools (022), approvals (021), MCP (060). @@ -37,7 +37,7 @@ Modularity promise: "adding a tool is adding a module". Everything in phases 2, - `lib/trinity/tools/{tool,result,context,registry,runner,schema}.ex`, `lib/trinity/tools.ex`, session ToolRunner swap, tests, docs. ## Acceptance criteria -1. [auto] Adding a tool module in `test/support` + one config line makes it appear in `Trinity.Tools.list/0` and in `to_llm_tools/0` with a valid JSON schema — with zero changes to core modules (diff shown). +1. [auto] Adding a tool module in `test/support` + one config line makes it appear in `Trinity.Tools.list/0` and in `to_llm_tools/0` with a valid JSON schema: with zero changes to core modules (diff shown). 2. [auto] FakeProvider emits two tool calls in one turn → both execute concurrently (Sleep 300 ms each; total < 500 ms) → two `tool` messages → final assistant message. 3. [auto] `Crash` tool → error result recorded; session continues; supervisor restart count unchanged. 4. [auto] `Sleep` beyond timeout → timeout error result within timeout + 100 ms. @@ -58,10 +58,10 @@ If that changes during the slice, the criterion is retagged and this section is - [ ] gate green · [ ] AC1–9 proven · [ ] ROADMAP → done · [ ] commit + tag ## Commit & tag -`feat(s020): complete slice 020 — tool protocol and registry` · tag `slice/020` +`feat(s020): complete slice 020 (tool protocol and registry)` · tag `slice/020` ## Risks / open questions -- Provider-specific tool-call quirks (parallel tool calls support) — capability flag from 011 `capabilities/1`. +- Provider-specific tool-call quirks (parallel tool calls support): capability flag from 011 `capabilities/1`. ## Platform alignment (appended 2026-09-05) - **Effect classification is part of the behaviour:** `effect/0 :: :none | :artifact | :catalog`. `:none` = read diff --git a/slices/021-permission-gate/SLICE.md b/slices/021-permission-gate/SLICE.md index bec6f38..9c60786 100644 --- a/slices/021-permission-gate/SLICE.md +++ b/slices/021-permission-gate/SLICE.md @@ -1,4 +1,4 @@ -# Slice 021 — Permission gate + approval UI +# Slice 021: Permission gate + approval UI | Field | Value | |---|---| @@ -49,14 +49,14 @@ Vision goal 5. Nothing side-effecting runs without consent. ## Manual verification queue Every `[manual]` criterion below needs a person. Listed here so the owner sees the queue at G1 rather than at review time. -- **AC2** — Write-risk tool → Session enters `approval_wait`; approval card renders (LiveView test + screenshot); "allow once" → tool runs → final message. -- **AC7** — Every decision has an `approvals` row with `decided_at`; `/permissions` lists them (screenshot). +- **AC2**: Write-risk tool → Session enters `approval_wait`; approval card renders (LiveView test + screenshot); "allow once" → tool runs → final message. +- **AC7**: Every decision has an `approvals` row with `decided_at`; `/permissions` lists them (screenshot). ## Definition of Done - [ ] gate green · [ ] AC1–8 proven · [ ] docs/07 synced · [ ] ROADMAP → done · [ ] commit + tag ## Commit & tag -`feat(s021): complete slice 021 — permission gate and approval UI` · tag `slice/021` +`feat(s021): complete slice 021 (permission gate and approval UI)` · tag `slice/021` ## Risks / open questions - Pattern language: start with glob for paths and prefix for commands; regex only via manual rule editing. diff --git a/slices/022-core-tools/SLICE.md b/slices/022-core-tools/SLICE.md index ec5cf54..9c65818 100644 --- a/slices/022-core-tools/SLICE.md +++ b/slices/022-core-tools/SLICE.md @@ -1,4 +1,4 @@ -# Slice 022 — Core tools: filesystem, web, shell +# Slice 022: Core tools: filesystem, web, shell | Field | Value | |---|---| @@ -17,9 +17,9 @@ M2 "Acts". Also closes the placeholder-overwrite class of data loss by design. ## Scope **In:** -- `Trinity.Tools.FS.{Read, Write, Edit, List, Glob, Grep}` — root allowlist from config + session cwd; `Write`/`Edit` risk `:write`; atomic writes; backup ring (last 5 per file under data dir); **write-validation hook** rejecting truncation markers unless `allow_placeholders: true` (which is `:destructive` risk). +- `Trinity.Tools.FS.{Read, Write, Edit, List, Glob, Grep}`: root allowlist from config + session cwd; `Write`/`Edit` risk `:write`; atomic writes; backup ring (last 5 per file under data dir); **write-validation hook** rejecting truncation markers unless `allow_placeholders: true` (which is `:destructive` risk). - `Trinity.Tools.Web.Fetch` (Req + Floki readability-style extraction, size cap, timeout, content-type handling, `<untrusted>` wrapping) and `Trinity.Tools.Web.Search` behind `Trinity.Tools.Web.SearchProvider` behaviour with one implementation (a provider chosen by the human: Brave/Tavily/Exa/DuckDuckGo-HTML; keyed via env) and a fake for tests. -- `Trinity.Tools.Shell.Run` — MuonTrap; cwd jail; env scrubbing (no secrets); timeout default 120 s; output cap 1 MB with tail retention; `:exec` risk; dangerous patterns → `:destructive`; background processes killed on Task exit (MuonTrap guarantees). +- `Trinity.Tools.Shell.Run`: MuonTrap; cwd jail; env scrubbing (no secrets); timeout default 120 s; output cap 1 MB with tail retention; `:exec` risk; dangerous patterns → `:destructive`; background processes killed on Task exit (MuonTrap guarantees). - Untrusted-content wrapping applied to all tool results that originate outside the app (web, shell output, file content) via `Trinity.Tools.Untrusted.wrap/2`. - Toolsets: `:fs`, `:web`, `:shell`. **Out:** @@ -52,14 +52,14 @@ M2 "Acts". Also closes the placeholder-overwrite class of data loss by design. ## Manual verification queue Every `[manual]` criterion below needs a person. Listed here so the owner sees the queue at G1 rather than at review time. -- **AC6** — `Web.Search` fake returns structured results; live-tagged test hits the real provider (redacted output). -- **AC10** — End-to-end manual: ask the agent to "list the files in the project and summarise the README" → works with approvals as expected (GIF). +- **AC6**: `Web.Search` fake returns structured results; live-tagged test hits the real provider (redacted output). +- **AC10**: End-to-end manual: ask the agent to "list the files in the project and summarise the README" → works with approvals as expected (GIF). ## Definition of Done - [ ] gate green · [ ] AC1–11 proven · [ ] docs/07 synced · [ ] VERSIONS (muontrap, floki ✅) · [ ] ROADMAP → done · [ ] commit + tag ## Commit & tag -`feat(s022): complete slice 022 — core tools (fs, web, shell)` · tag `slice/022` +`feat(s022): complete slice 022 (core tools: fs, web, shell)` · tag `slice/022` ## Risks / open questions - **Windows shell: decide before this slice, not inside it.** Windows is first class elsewhere (001 AC3, 100 AC1). diff --git a/slices/023-context-compaction/SLICE.md b/slices/023-context-compaction/SLICE.md index fafb013..e31f4bd 100644 --- a/slices/023-context-compaction/SLICE.md +++ b/slices/023-context-compaction/SLICE.md @@ -1,4 +1,4 @@ -# Slice 023 — Context compaction + session lineage +# Slice 023: Context compaction + session lineage | Field | Value | |---|---| @@ -17,15 +17,15 @@ Risk R7. Compression that quietly discards critical context is the failure to de ## Scope **In:** -- `Trinity.Memory.Tokens` — token estimation per model (provider tokenizer if req_llm exposes; else calibrated heuristic). -- `Trinity.Memory.Compactor` — strategy: keep system + last K turns; summarise older turns via `Trinity.LLM.generate_object/3` into `%{summary, open_threads, decisions, facts}`; write a `system`-role `compaction` message; mark compacted messages `parts.compacted_by`; optionally fork a child session (`parent_id`) when history exceeds a hard limit. +- `Trinity.Memory.Tokens`: token estimation per model (provider tokenizer if req_llm exposes; else calibrated heuristic). +- `Trinity.Memory.Compactor`: strategy: keep system + last K turns; summarise older turns via `Trinity.LLM.generate_object/3` into `%{summary, open_threads, decisions, facts}`; write a `system`-role `compaction` message; mark compacted messages `parts.compacted_by`; optionally fork a child session (`parent_id`) when history exceeds a hard limit. - Session integration: `compacting` state triggered when estimated prompt > threshold (per model) before a turn; prompt builder uses the compaction message + uncompacted tail. - UI: "context: N / M tokens" indicator; compaction event shown as a collapsible card; "view original" link to the parent/compacted messages. - Eval harness built to take suites beyond this one. Compaction is its first; tool selection, injection resistance and memory recall are the next, added by the slices that own them rather than here. -- Eval harness: `test/evals/compaction/*.exs` with 3 scripted long conversations and assertions that named facts survive (keyword presence) — run with `mix test --only eval` (excluded by default), results table saved to `proof/`. +- Eval harness: `test/evals/compaction/*.exs` with 3 scripted long conversations and assertions that named facts survive (keyword presence): run with `mix test --only eval` (excluded by default), results table saved to `proof/`. **Out:** -- Semantic memory extraction (032) — compaction may *emit* candidate memories to a queue consumed there. +- Semantic memory extraction (032): compaction may *emit* candidate memories to a queue consumed there. ## Design notes - Never delete messages. Compaction adds; lineage points back. @@ -48,14 +48,14 @@ Risk R7. Compression that quietly discards critical context is the failure to de ## Manual verification queue Every `[manual]` criterion below needs a person. Listed here so the owner sees the queue at G1 rather than at review time. -- **AC4** — Eval harness: ≥ 90 % of tracked facts survive across the 3 scripted conversations with a real model (live/eval tag; table in proof). -- **AC5** — UI shows the token indicator and compaction card (screenshot). +- **AC4**: Eval harness: ≥ 90 % of tracked facts survive across the 3 scripted conversations with a real model (live/eval tag; table in proof). +- **AC5**: UI shows the token indicator and compaction card (screenshot). ## Definition of Done - [ ] gate green · [ ] AC1–6 proven · [ ] docs/05 synced · [ ] ROADMAP → done · [ ] commit + tag ## Commit & tag -`feat(s023): complete slice 023 — context compaction and lineage` · tag `slice/023` +`feat(s023): complete slice 023 (context compaction and lineage)` · tag `slice/023` ## Risks / open questions - Tokenizer accuracy per provider; calibrate against live `usage` numbers and record the error margin. diff --git a/slices/024-effect-catalog-authority-modes-receipts/SLICE.md b/slices/024-effect-catalog-authority-modes-receipts/SLICE.md index 0142a15..46d7926 100644 --- a/slices/024-effect-catalog-authority-modes-receipts/SLICE.md +++ b/slices/024-effect-catalog-authority-modes-receipts/SLICE.md @@ -1,4 +1,4 @@ -# Slice 024 — Effect catalog, authority selection, local receipts +# Slice 024: Effect catalog, authority selection, local receipts | Field | Value | |---|---| @@ -22,7 +22,7 @@ property, and "Trinity keeps no executor for delegated effects" is unfalsifiable - `Trinity.Effects` boundary: the only module that invokes a tool's `execute/2` for `effect != :none`; revalidates policy decision, approval fingerprint (M2), idempotency key, scope, and authority mode before executing; denies and receipts on any mismatch. A census test asserts no other caller of `execute/2` exists for effectful tools - (plant a bypass module in test; census must flag it — the F6 pattern). + (plant a bypass module in test; census must flag it: the F6 pattern). - `Trinity.Effects.Catalog` compile-time module attribute; `Trinity.CorePolicy.hash/0` = digest over the policy, catalog and gate modules' object code, recorded in the boot receipt. - `Trinity.Authority` behaviour: `stage/2`, `decide/3`, `execute/3`, `receipt/2`; the `Local` implementation; @@ -73,7 +73,7 @@ If that changes during the slice, the criterion is retagged and this section is - [ ] gate green · [ ] AC1–7 proven · [ ] docs/01, docs/05, docs/07 synced · [ ] ROADMAP → done · [ ] commit + tag ## Commit & tag -`feat(s024): complete slice 024 — effect catalog, authority selection, local receipts` · tag `slice/024` +`feat(s024): complete slice 024 (effect catalog, authority selection, local receipts)` · tag `slice/024` ## Legal review (before G1) The `signed_payload` field set goes to the owner for legal review before this slice starts (R21). Default design diff --git a/slices/030-persona-always-on-memory/SLICE.md b/slices/030-persona-always-on-memory/SLICE.md index 97522d6..e56b067 100644 --- a/slices/030-persona-always-on-memory/SLICE.md +++ b/slices/030-persona-always-on-memory/SLICE.md @@ -1,4 +1,4 @@ -# Slice 030 — Persona (SOUL) + always-on memory tier +# Slice 030: Persona (SOUL) + always-on memory tier | Field | Value | |---|---| @@ -19,12 +19,12 @@ Vision goal 3, first half. A small always-on tier with a budget that consolidate **In:** - `Trinity.Personas` context: CRUD, `priv/personas/default/SOUL.md` seeded on first run; `sessions.persona_id`; persona picker in UI; `/personality`-style quick edits stored as persona settings. - `Trinity.Memory` context (always-on part): `memories` table (`tier`, `scope`, `key`, `body`); `Trinity.Memory.AlwaysOn.snapshot/1` renders a deterministic block for the prompt (sorted, sized). -- `memory` tool: `add(tier, key, body)`, `replace(key, body)`, `remove(key)`, `list()` — risk `:write` with a persona-level default rule "allow" (memory writes are low risk but auditable); every change logged. +- `memory` tool: `add(tier, key, body)`, `replace(key, body)`, `remove(key)`, `list()`: risk `:write` with a persona-level default rule "allow" (memory writes are low risk but auditable); every change logged. - Budget: per persona (default 8 KB total for profile + always_on). When exceeded, `Trinity.Memory.Consolidator` asks the LLM to merge/condense entries into a proposal; the proposal is applied automatically if under budget, else queued for user review (UI list). -- Prompt builder ordering: stable (SOUL, tool guidance) → context (skills index placeholder) → volatile (memory snapshot, time, session facts) — mirroring the caching-friendly tiering. +- Prompt builder ordering: stable (SOUL, tool guidance) → context (skills index placeholder) → volatile (memory snapshot, time, session facts), mirroring the caching-friendly tiering. - UI: persona editor (SOUL markdown), memory panel (profile / always-on lists with inline edit and delete), consolidation review. **Out:** -- Semantic/retrievable memory (032), FTS (031), project-scoped memory files (`AGENTS.md`-style — noted as follow-up). +- Semantic/retrievable memory (032), FTS (031), project-scoped memory files (`AGENTS.md`-style: noted as follow-up). ## Design notes - Snapshot is computed once per session start and on explicit refresh; the Session stores it in state so mid-session edits do not silently change behaviour (documented UX: "takes effect next session or on refresh"). @@ -47,13 +47,13 @@ Vision goal 3, first half. A small always-on tier with a budget that consolidate ## Manual verification queue Every `[manual]` criterion below needs a person. Listed here so the owner sees the queue at G1 rather than at review time. -- **AC5** — UI: edit SOUL, add/delete memory entries (screenshots); changes persist. +- **AC5**: UI: edit SOUL, add/delete memory entries (screenshots); changes persist. ## Definition of Done - [ ] gate green · [ ] AC1–7 proven · [ ] docs/05 synced · [ ] ROADMAP → done · [ ] commit + tag ## Commit & tag -`feat(s030): complete slice 030 — persona and always-on memory` · tag `slice/030` +`feat(s030): complete slice 030 (persona and always-on memory)` · tag `slice/030` ## Risks / open questions - Auto-applying consolidation may surprise users; default to "auto if under budget, else review" and make it configurable. diff --git a/slices/031-session-search-fts/SLICE.md b/slices/031-session-search-fts/SLICE.md index c48549e..d4938f7 100644 --- a/slices/031-session-search-fts/SLICE.md +++ b/slices/031-session-search-fts/SLICE.md @@ -1,4 +1,4 @@ -# Slice 031 — Session search (FTS5) +# Slice 031: Session search (FTS5) | Field | Value | |---|---| @@ -40,14 +40,14 @@ equivalent; `Trinity.Memory.Search.messages/2`; a `session_search` tool; a searc ## Manual verification queue Every `[manual]` criterion below needs a person. Listed here so the owner sees the queue at G1 rather than at review time. -- **AC4** — Tool returns ≤ `limit` hits with snippets; agent can answer "what did we decide about X last week" using it (manual GIF). -- **AC5** — Search page renders results and deep-links (screenshot). +- **AC4**: Tool returns ≤ `limit` hits with snippets; agent can answer "what did we decide about X last week" using it (manual GIF). +- **AC5**: Search page renders results and deep-links (screenshot). ## Definition of Done - [ ] gate green · [ ] AC1–5 proven · [ ] docs/05 synced · [ ] ROADMAP → done · [ ] commit + tag ## Commit & tag -`feat(s031): complete slice 031 — session search (FTS5)` · tag `slice/031` +`feat(s031): complete slice 031 (session search, FTS5)` · tag `slice/031` ## Risks / open questions -- FTS5 must be compiled into the bundled SQLite (exqlite default builds include it — verify in the Burrito binary during 100). +- FTS5 must be compiled into the bundled SQLite (exqlite default builds include it: verify in the Burrito binary during 100). diff --git a/slices/032-semantic-memory/SLICE.md b/slices/032-semantic-memory/SLICE.md index 6adda14..c7eec2c 100644 --- a/slices/032-semantic-memory/SLICE.md +++ b/slices/032-semantic-memory/SLICE.md @@ -1,4 +1,4 @@ -# Slice 032 — Embeddings, semantic memory, hybrid retrieval +# Slice 032: Embeddings, semantic memory, hybrid retrieval | Field | Value | |---|---| @@ -24,13 +24,13 @@ Vision goal 3, second half. The unbounded tier that makes the always-on tier's s - Retrieval: `Trinity.Memory.Retriever.relevant(session, query, k)` = RRF(FTS hits, vector hits) with recency decay; result block rendered into the volatile prompt tier under a token cap. - `recall(query, k)` tool, risk `:read`. - UI: memory panel gains a "semantic" tab with search, provenance links, delete/pin (pin = promote to always_on). -- Measurements: embed latency (single/batched), EXLA/Bumblebee binary size impact, RAM at idle and during embed — recorded in `docs/perf.md` (new). +- Measurements: embed latency (single/batched), EXLA/Bumblebee binary size impact, RAM at idle and during embed, recorded in `docs/perf.md` (new). **Out:** - Knowledge-graph memory, reranking models, project-scoped indexes (follow-ups). ## Design notes - Model weights cached under the data dir (`BUMBLEBEE_CACHE_DIR`); first-run download with UI progress; offline fallback = hosted embedder or disabled semantic tier (never crash). -- `sqlite_vec` extension load happens in a Repo `after_connect` hook; verify it survives Burrito packaging (R4) — do the check in this slice by building a Burrito binary and running the vec test inside it. +- `sqlite_vec` extension load happens in a Repo `after_connect` hook; verify it survives Burrito packaging (R4): do the check in this slice by building a Burrito binary and running the vec test inside it. ## Deliverables - `lib/trinity/memory/{embedder,embedders/*,vector_store,vector_stores/*,observer,retriever}.ex`, migrations (vec table; pgvector column), tool, UI tab, `docs/perf.md`, tests with a tiny fake embedder (deterministic vectors). @@ -42,7 +42,7 @@ Vision goal 3, second half. The unbounded tier that makes the always-on tier's s 4. [auto] Retriever: FTS-only hit and vector-only hit both appear in fused results; recency decay demoted an old identical memory (test with controlled data). 5. [auto] Prompt contains a "Relevant memories" block bounded by the token cap (prompt snapshot test). 6. [manual] `recall` tool works end-to-end (manual GIF: teach a fact in one session, recall it in a new one). -7. [auto] Packaged Burrito binary loads `sqlite_vec` and passes a vec smoke test (log) — or R4 fallback implemented and documented. +7. [auto] Packaged Burrito binary loads `sqlite_vec` and passes a vec smoke test (log), or R4 fallback implemented and documented. 8. [auto] Perf table recorded (latency, binary size delta, RAM). ## Proof required @@ -51,14 +51,14 @@ Vision goal 3, second half. The unbounded tier that makes the always-on tier's s ## Manual verification queue Every `[manual]` criterion below needs a person. Listed here so the owner sees the queue at G1 rather than at review time. -- **AC2** — Real Bumblebee embedder: `dim/0 == 384`; embedding "the cat sat" vs "a cat was sitting" cosine > 0.7; vs "quarterly tax filing" < 0.3 (live/slow…. -- **AC6** — `recall` tool works end-to-end (manual GIF: teach a fact in one session, recall it in a new one). +- **AC2**: Real Bumblebee embedder: `dim/0 == 384`; embedding "the cat sat" vs "a cat was sitting" cosine > 0.7; vs "quarterly tax filing" < 0.3 (live/slow…. +- **AC6**: `recall` tool works end-to-end (manual GIF: teach a fact in one session, recall it in a new one). ## Definition of Done - [ ] gate green · [ ] AC1–8 proven · [ ] docs/05, docs/perf.md, VERSIONS (bumblebee, nx, exla, sqlite_vec ✅) · [ ] ROADMAP → done · [ ] commit + tag ## Commit & tag -`feat(s032): complete slice 032 — semantic memory and hybrid retrieval` · tag `slice/032` +`feat(s032): complete slice 032 (semantic memory and hybrid retrieval)` · tag `slice/032` ## Risks / open questions - R3/R4 are decided here. If EXLA is too heavy, ship with hosted embeddings default and local as opt-in. diff --git a/slices/033-project-context-agents-md/SLICE.md b/slices/033-project-context-agents-md/SLICE.md index e8a7da2..557fe52 100644 --- a/slices/033-project-context-agents-md/SLICE.md +++ b/slices/033-project-context-agents-md/SLICE.md @@ -1,4 +1,4 @@ -# Slice 033 — Project context: AGENTS.md +# Slice 033: Project context: AGENTS.md | Field | Value | |---|---| @@ -41,4 +41,4 @@ If that changes during the slice, the criterion is retagged and this section is - [ ] `mix gate` green · [ ] AC1–3 proven · [ ] docs/ADR/VERSIONS updated if affected · [ ] ROADMAP status → done · [ ] final commit + tag ## Commit & tag -`feat(s033): complete slice 033 — AGENTS.md project context` · tag `slice/033` +`feat(s033): complete slice 033 (AGENTS.md project context)` · tag `slice/033` diff --git a/slices/034-export-import-restore/SLICE.md b/slices/034-export-import-restore/SLICE.md index bee41ba..5c55b4b 100644 --- a/slices/034-export-import-restore/SLICE.md +++ b/slices/034-export-import-restore/SLICE.md @@ -1,4 +1,4 @@ -# Slice 034 — Export, import, restore +# Slice 034: Export, import, restore | Field | Value | |---|---| @@ -62,7 +62,7 @@ If that changes during the slice, the criterion is retagged and this section is - [ ] `mix gate` green · [ ] AC1–6 proven · [ ] `docs/backup.md` written · [ ] ROADMAP status → done · [ ] final commit + tag ## Commit & tag -`feat(s034): complete slice 034 — export, import, restore` · tag `slice/034` +`feat(s034): complete slice 034 (export, import, restore)` · tag `slice/034` ## Risks / open questions - Archive size once embeddings and model caches exist. Measure and decide what is excluded by default. diff --git a/slices/040-skills-registry/SLICE.md b/slices/040-skills-registry/SLICE.md index 5b45330..39e4792 100644 --- a/slices/040-skills-registry/SLICE.md +++ b/slices/040-skills-registry/SLICE.md @@ -1,4 +1,4 @@ -# Slice 040 — Skills registry, SKILL.md format, progressive disclosure +# Slice 040: Skills registry, SKILL.md format, progressive disclosure | Field | Value | |---|---| @@ -20,7 +20,7 @@ Vision goal 4. Procedural memory that costs ~nothing until used. **In:** - `Trinity.Skills.Skill` struct + `Trinity.Skills.Parser` (YAML frontmatter via `yaml_elixir`, body, `references/`, `scripts/` listing); validation with clear errors; Trinity extensions under `trinity:` key (`requires_tools`, `requires_toolsets`, `fallback_for_toolsets`, `risk`, `lua_entry`). - `Trinity.Skills.Registry` (GenServer + ETS) scanning sources in precedence order project → user → bundled; `FileSystem` watcher for hot reload; `mix trinity.skills.reindex`. -- `skills` table as index (name, version, source, path, frontmatter, body_hash, status, scan_result) — filesystem canonical. +- `skills` table as index (name, version, source, path, frontmatter, body_hash, status, scan_result): filesystem canonical. - Tools: `skills_list()` → compact index (name + one-line description, grouped by category; token-capped), `skill_view(name)` → SKILL.md body, `skill_file(name, path)` → reference file (path jailed to the skill dir). All `:read`. - Prompt: the skills index goes into the context tier with a cap (e.g. 2–3k tokens); over cap → categories only + hint to call `skills_list`. - Conditional activation: skills whose `requires_tools` are unavailable are hidden; `fallback_for_toolsets` shown only when those toolsets are disabled. @@ -52,18 +52,18 @@ Vision goal 4. Procedural memory that costs ~nothing until used. ## Manual verification queue Every `[manual]` criterion below needs a person. Listed here so the owner sees the queue at G1 rather than at review time. -- **AC3** — Hot reload: modifying a SKILL.md on disk updates the registry within 2 s without restart (test with watcher; or manual proof if watcher…. -- **AC7** — Manual: agent, asked to do a git task, calls `skill_view("git-workflow")` then follows it (GIF). -- **AC8** — An agentskills.io skill written for another agent, downloaded by the human, parses and appears (proof: name + source). +- **AC3**: Hot reload: modifying a SKILL.md on disk updates the registry within 2 s without restart (test with watcher; or manual proof if watcher…. +- **AC7**: Manual: agent, asked to do a git task, calls `skill_view("git-workflow")` then follows it (GIF). +- **AC8**: An agentskills.io skill written for another agent, downloaded by the human, parses and appears (proof: name + source). ## Definition of Done - [ ] gate green · [ ] AC1–8 proven · [ ] docs/05 synced · [ ] ROADMAP → done · [ ] commit + tag ## Commit & tag -`feat(s040): complete slice 040 — skills registry and progressive disclosure` · tag `slice/040` +`feat(s040): complete slice 040 (skills registry and progressive disclosure)` · tag `slice/040` ## Risks / open questions -- `file_system` watcher on Windows/macOS inside a packaged app — verify in 100; reindex button is the fallback. +- `file_system` watcher on Windows/macOS inside a packaged app: verify in 100; reindex button is the fallback. ## Platform alignment (appended 2026-09-05) - **Content digest:** `skills.body_hash` plus a per-file digest manifest under the skill dir; the diff --git a/slices/041-skill-self-management/SLICE.md b/slices/041-skill-self-management/SLICE.md index b6bb82f..d6df591 100644 --- a/slices/041-skill-self-management/SLICE.md +++ b/slices/041-skill-self-management/SLICE.md @@ -1,4 +1,4 @@ -# Slice 041 — Skill self-management with staged approval + scanner +# Slice 041: Skill self-management with staged approval + scanner | Field | Value | |---|---| @@ -9,7 +9,7 @@ ## Goal The agent can propose new skills and edits to existing ones (`skill_manage` tool: create/patch/write_file/ -remove_file/delete) — staged as `skill_changes` with diff and rationale, scanned for dangerous content, shown in an +remove_file/delete): staged as `skill_changes` with diff and rationale, scanned for dangerous content, shown in an approval UI, and applied to the filesystem only on approval. Plus a `/learn` flow that distils a document/URL into a knowledge skill (SKILL.md + `references/`). @@ -50,17 +50,17 @@ Vision goal 4: "grows safely". A self-improving skills library is only safe if t ## Manual verification queue Every `[manual]` criterion below needs a person. Listed here so the owner sees the queue at G1 rather than at review time. -- **AC6** — `/learn` with a local markdown file produces a staged knowledge skill with a `references/` file and a SKILL.md under ~200 lines (live/eval tag;…. -- **AC7** — UI screenshots: pending list, diff view, findings. +- **AC6**: `/learn` with a local markdown file produces a staged knowledge skill with a `references/` file and a SKILL.md under ~200 lines (live/eval tag;…. +- **AC7**: UI screenshots: pending list, diff view, findings. ## Definition of Done - [ ] gate green · [ ] AC1–8 proven · [ ] docs/07 synced · [ ] ROADMAP → done · [ ] commit + tag ## Commit & tag -`feat(s041): complete slice 041 — skill self-management with approval` · tag `slice/041` +`feat(s041): complete slice 041 (skill self-management with approval)` · tag `slice/041` ## Risks / open questions -- Diff quality for binary/reference files — treat non-text as replace-whole with a size note. +- Diff quality for binary/reference files: treat non-text as replace-whole with a size note. ## Platform alignment (appended 2026-09-05) - **Promotion is a gated artifact effect:** approve/reject of a staged change goes through diff --git a/slices/050-scheduler-oban/SLICE.md b/slices/050-scheduler-oban/SLICE.md index fb80864..e11e539 100644 --- a/slices/050-scheduler-oban/SLICE.md +++ b/slices/050-scheduler-oban/SLICE.md @@ -1,4 +1,4 @@ -# Slice 050 — Scheduler: Oban cron agent tasks with delivery targets +# Slice 050: Scheduler: Oban cron agent tasks with delivery targets | Field | Value | |---|---| @@ -49,15 +49,15 @@ Scheduled work as durable, retried, observable jobs rather than entries in a con ## Manual verification queue Every `[manual]` criterion below needs a person. Listed here so the owner sees the queue at G1 rather than at review time. -- **AC2** — `RunTask` creates a session with `origin: "cron"`, completes a FakeProvider turn, records a `task_runs` row with summary, and delivers a desktop…. -- **AC6** — Memory observer runs as an Oban job and is visible in Oban Web (screenshot). -- **AC7** — Manual: create a "daily summary of my notes dir" task, run now, see the result (GIF). +- **AC2**: `RunTask` creates a session with `origin: "cron"`, completes a FakeProvider turn, records a `task_runs` row with summary, and delivers a desktop…. +- **AC6**: Memory observer runs as an Oban job and is visible in Oban Web (screenshot). +- **AC7**: Manual: create a "daily summary of my notes dir" task, run now, see the result (GIF). ## Definition of Done - [ ] gate green · [ ] AC1–7 proven · [ ] docs/05 synced · [ ] VERSIONS (oban ✅) · [ ] ROADMAP → done · [ ] commit + tag ## Commit & tag -`feat(s050): complete slice 050 — scheduler with Oban cron agent tasks` · tag `slice/050` +`feat(s050): complete slice 050 (scheduler with Oban cron agent tasks)` · tag `slice/050` ## Risks / open questions -- Oban Lite + `ecto_sqlite3` pool contention with the app's writes — measure under the 010 stress test with Oban running. +- Oban Lite + `ecto_sqlite3` pool contention with the app's writes: measure under the 010 stress test with Oban running. diff --git a/slices/059-mcp-library-spike/SLICE.md b/slices/059-mcp-library-spike/SLICE.md index a672260..c026947 100644 --- a/slices/059-mcp-library-spike/SLICE.md +++ b/slices/059-mcp-library-spike/SLICE.md @@ -1,4 +1,4 @@ -# Slice 059 — MCP library spike (finalises ADR-0007) +# Slice 059: MCP library spike (finalises ADR-0007) | Field | Value | |---|---| @@ -53,4 +53,4 @@ If that changes during the slice, the criterion is retagged and this section is - [ ] `mix gate` green · [ ] AC1–3 proven · [ ] docs/ADR/VERSIONS updated if affected · [ ] ROADMAP status → done · [ ] final commit + tag ## Commit & tag -`feat(s059): complete slice 059 — MCP library spike` · tag `slice/059` +`feat(s059): complete slice 059 (MCP library spike)` · tag `slice/059` diff --git a/slices/060-mcp-client/SLICE.md b/slices/060-mcp-client/SLICE.md index 2fe57ba..306df8b 100644 --- a/slices/060-mcp-client/SLICE.md +++ b/slices/060-mcp-client/SLICE.md @@ -1,4 +1,4 @@ -# Slice 060 — MCP client (2026-07-28 preferred, 2025-11-25 compat) +# Slice 060: MCP client (2026-07-28 preferred, 2025-11-25 compat) | Field | Value | |---|---| @@ -12,7 +12,7 @@ sampling/elicitation callbacks (both deprecated in 2026-07-28). **Sized `M/L`, conditionally, and the condition is decided by slice 059.** M if 059 selects a library that ships a working client. L otherwise: a server-only library, or the own-minimal-server fallback, leaves this slice to build -the MRTR retry loop, Tasks polling and the whole OAuth client role — PKCE, resource indicators, client metadata +the MRTR retry loop, Tasks polling and the whole OAuth client role: PKCE, resource indicators, client metadata with dynamic-registration fallback, issuer checking and per-issuer credential storage. `docs/08-standards.md` says as much in its own words: the stateless server side is small and the client side is more work. A single number here would be a guess wearing an estimate's clothes. @@ -58,10 +58,10 @@ enter the effect catalog (M4); health, reconnect, UI. ## Manual verification queue Every `[manual]` criterion below needs a person. Listed here so the owner sees the queue at G1 rather than at review time. -- **AC8** — Manual: one real public 2026-07-28 server used end-to-end (GIF). +- **AC8**: Manual: one real public 2026-07-28 server used end-to-end (GIF). ## Definition of Done - [ ] gate green · [ ] AC1–8 proven · [ ] docs/01, docs/08 synced · [ ] VERSIONS ✅ · [ ] ROADMAP → done · [ ] commit + tag ## Commit & tag -`feat(s060): complete slice 060 — MCP client` · tag `slice/060` +`feat(s060): complete slice 060 (MCP client)` · tag `slice/060` diff --git a/slices/061-mcp-server/SLICE.md b/slices/061-mcp-server/SLICE.md index 03fbe50..39cb17e 100644 --- a/slices/061-mcp-server/SLICE.md +++ b/slices/061-mcp-server/SLICE.md @@ -1,4 +1,4 @@ -# Slice 061 — MCP server (stateless 2026-07-28, compat for 2025-11-25) +# Slice 061: MCP server (stateless 2026-07-28, compat for 2025-11-25) | Field | Value | |---|---| @@ -30,7 +30,7 @@ attributed to a system persona with `origin: "mcp"` and crossing `Trinity.Effect - Auth: `Trinity.MCP.Auth.Local` default (loopback bind + static token); the full RS profile is slice 062. - `MIX_ENV=prod TRINITY_MODE=headless` release: no Tauri, no LiveView required, MCP + HTTP API only; systemd unit example. - Docs page with connection snippets for common clients (Claude Code, Codex, goose, VS Code). -**Out:** MCP Apps (server-rendered UI) — follow-up; internet exposure. +**Out:** MCP Apps (server-rendered UI): follow-up; internet exposure. ## Acceptance criteria 1. [auto] Our 060 client connects at 2026-07-28 and lists exported tools; a 2025-11-25 test client connects to the same @@ -47,10 +47,10 @@ attributed to a system persona with `origin: "mcp"` and crossing `Trinity.Effect ## Manual verification queue Every `[manual]` criterion below needs a person. Listed here so the owner sees the queue at G1 rather than at review time. -- **AC8** — Claude Code connected and calling `recall` (screenshot). +- **AC8**: Claude Code connected and calling `recall` (screenshot). ## Definition of Done - [ ] gate green · [ ] AC1–8 proven · [ ] docs synced · [ ] ROADMAP → done · [ ] commit + tag ## Commit & tag -`feat(s061): complete slice 061 — MCP server` · tag `slice/061` +`feat(s061): complete slice 061 (MCP server)` · tag `slice/061` diff --git a/slices/062-mcp-server-auth/SLICE.md b/slices/062-mcp-server-auth/SLICE.md index a6040ac..652d888 100644 --- a/slices/062-mcp-server-auth/SLICE.md +++ b/slices/062-mcp-server-auth/SLICE.md @@ -1,4 +1,4 @@ -# Slice 062 — MCP authorization: resource server, embedded authorization server, Enterprise Managed Authorization +# Slice 062: MCP authorization: resource server, embedded authorization server, Enterprise Managed Authorization | Field | Value | |---|---| @@ -49,13 +49,13 @@ clients (CIMD URLs), scopes; conformance tests modelled on the spec's flows; doc ## Manual verification queue Every `[manual]` criterion below needs a person. Listed here so the owner sees the queue at G1 rather than at review time. -- **AC6** — Manual: one real MCP client that supports EMA (per the MCP client matrix at the time) connects through the fake IdP. +- **AC6**: Manual: one real MCP client that supports EMA (per the MCP client matrix at the time) connects through the fake IdP. ## Definition of Done - [ ] gate green · [ ] AC1–7 proven · [ ] docs/08 synced · [ ] ROADMAP → done · [ ] commit + tag ## Commit & tag -`feat(s062): complete slice 062 — MCP authorization (RS, embedded AS, EMA)` · tag `slice/062` +`feat(s062): complete slice 062 (MCP authorization: RS, embedded AS, EMA)` · tag `slice/062` ## Risks / open questions - R23: ID-JAG draft revision pinned in NOTES.md; re-check at each phase boundary. diff --git a/slices/070-gateway-core/SLICE.md b/slices/070-gateway-core/SLICE.md index cba17e9..5de1896 100644 --- a/slices/070-gateway-core/SLICE.md +++ b/slices/070-gateway-core/SLICE.md @@ -1,4 +1,4 @@ -# Slice 070 — Gateway core: adapter behaviour, routing, PubSub fan-out +# Slice 070: Gateway core: adapter behaviour, routing, PubSub fan-out | Field | Value | |---|---| @@ -32,8 +32,8 @@ Vision goal 2. Gateways are PubSub subscribers in the same node, not a separate - Real platforms (071, 072), voice transcription (follow-up), media uploads beyond images. ## Design notes -- Adapters never call the LLM or Sessions directly — only Router. -- Session linking: a gateway conversation can attach to an existing desktop session via `/attach <session_id>` — both surfaces then see the same stream (this is the demo). +- Adapters never call the LLM or Sessions directly: only Router. +- Session linking: a gateway conversation can attach to an existing desktop session via `/attach <session_id>`: both surfaces then see the same stream (this is the demo). ## Deliverables - `lib/trinity/gateways/{adapter,router,pairing,commands,format,console}.ex`, `lib/trinity/gateways.ex`, migration (`gateway_identities`), UI, tests. @@ -55,11 +55,11 @@ Vision goal 2. Gateways are PubSub subscribers in the same node, not a separate ## Manual verification queue Every `[manual]` criterion below needs a person. Listed here so the owner sees the queue at G1 rather than at review time. -- **AC2** — Unpaired identity receives only a pairing prompt; after entering the code shown in the UI, the next message is processed (test + screenshot). -- **AC9** — `/gateways` UI screenshot. +- **AC2**: Unpaired identity receives only a pairing prompt; after entering the code shown in the UI, the next message is processed (test + screenshot). +- **AC9**: `/gateways` UI screenshot. ## Definition of Done - [ ] gate green · [ ] AC1–9 proven · [ ] docs/01, docs/07 synced · [ ] ROADMAP → done · [ ] commit + tag ## Commit & tag -`feat(s070): complete slice 070 — gateway core` · tag `slice/070` +`feat(s070): complete slice 070 (gateway core)` · tag `slice/070` diff --git a/slices/071-gateway-telegram/SLICE.md b/slices/071-gateway-telegram/SLICE.md index d7ee2a8..b4497ae 100644 --- a/slices/071-gateway-telegram/SLICE.md +++ b/slices/071-gateway-telegram/SLICE.md @@ -1,4 +1,4 @@ -# Slice 071 — Gateway: Telegram +# Slice 071: Gateway: Telegram | Field | Value | |---|---| @@ -8,7 +8,7 @@ | Depends on | 070 | ## Goal -A Telegram adapter (Telegex or ex_gram — choose in NOTES with justification; long-polling by default, webhook +A Telegram adapter (Telegex or ex_gram; choose in NOTES with justification; long-polling by default, webhook optional) supporting DMs and group mentions, streaming via message edits (throttled), inline approval buttons, images in/out, and the cron delivery target. @@ -17,7 +17,7 @@ images in/out, and the cron delivery target. **Out:** voice notes (follow-up: Whisper via Bumblebee), stickers/polls. ## Acceptance criteria -1. [manual] Live-tagged test (or manual with proof) — send a DM, get a streamed reply that updates in place, ending with the final text (screenshot sequence). +1. [manual] Live-tagged test (or manual with proof): send a DM, get a streamed reply that updates in place, ending with the final text (screenshot sequence). 2. [manual] Approval buttons work from Telegram and the desktop UI reflects the decision (screenshot). 3. [manual] An image sent to the bot is stored and passed to a vision-capable model; the reply references it (live/manual proof). 4. [auto] Unit tests for formatting/escaping/chunking with tricky markdown (code blocks, underscores, links). @@ -27,13 +27,13 @@ images in/out, and the cron delivery target. ## Manual verification queue Every `[manual]` criterion below needs a person. Listed here so the owner sees the queue at G1 rather than at review time. -- **AC1** — Live-tagged test (or manual with proof) — send a DM, get a streamed reply that updates in place, ending with the final text (screenshot sequence). -- **AC2** — Approval buttons work from Telegram and the desktop UI reflects the decision (screenshot). -- **AC3** — An image sent to the bot is stored and passed to a vision-capable model; the reply references it (live/manual proof). -- **AC6** — Cron task delivers to Telegram (manual proof). +- **AC1**: Live-tagged test (or manual with proof). Send a DM, get a streamed reply that updates in place, ending with the final text (screenshot sequence). +- **AC2**: Approval buttons work from Telegram and the desktop UI reflects the decision (screenshot). +- **AC3**: An image sent to the bot is stored and passed to a vision-capable model; the reply references it (live/manual proof). +- **AC6**: Cron task delivers to Telegram (manual proof). ## Definition of Done - [ ] gate green · [ ] AC1–6 proven · [ ] VERSIONS (telegram lib ✅) · [ ] ROADMAP → done · [ ] commit + tag ## Commit & tag -`feat(s071): complete slice 071 — Telegram gateway` · tag `slice/071` +`feat(s071): complete slice 071 (Telegram gateway)` · tag `slice/071` diff --git a/slices/072-gateway-discord/SLICE.md b/slices/072-gateway-discord/SLICE.md index 3bbb04b..f59c132 100644 --- a/slices/072-gateway-discord/SLICE.md +++ b/slices/072-gateway-discord/SLICE.md @@ -1,4 +1,4 @@ -# Slice 072 — Gateway: Discord (Nostrum) +# Slice 072: Gateway: Discord (Nostrum) | Field | Value | |---|---| @@ -26,13 +26,13 @@ attachments in/out, slash commands registered with Discord, cron delivery target ## Manual verification queue Every `[manual]` criterion below needs a person. Listed here so the owner sees the queue at G1 rather than at review time. -- **AC1** — Manual/live proof: mention the bot in a channel → threaded streamed reply (screenshots). -- **AC2** — Button approvals round-trip (screenshot). -- **AC3** — Slash commands `/new`, `/model` work and are registered (screenshot of Discord command list). -- **AC6** — Cron delivery to a channel (manual proof). +- **AC1**: Manual/live proof: mention the bot in a channel → threaded streamed reply (screenshots). +- **AC2**: Button approvals round-trip (screenshot). +- **AC3**: Slash commands `/new`, `/model` work and are registered (screenshot of Discord command list). +- **AC6**: Cron delivery to a channel (manual proof). ## Definition of Done - [ ] gate green · [ ] AC1–6 proven · [ ] VERSIONS (nostrum ✅) · [ ] ROADMAP → done · [ ] commit + tag ## Commit & tag -`feat(s072): complete slice 072 — Discord gateway` · tag `slice/072` +`feat(s072): complete slice 072 (Discord gateway)` · tag `slice/072` diff --git a/slices/080-subagents/SLICE.md b/slices/080-subagents/SLICE.md index 241da2c..f81c487 100644 --- a/slices/080-subagents/SLICE.md +++ b/slices/080-subagents/SLICE.md @@ -1,4 +1,4 @@ -# Slice 080 — Subagents + delegation +# Slice 080: Subagents + delegation | Field | Value | |---|---| @@ -10,7 +10,7 @@ ## Goal A `delegate` tool that spawns supervised child sessions (subagents) with a scoped brief, restricted toolsets, their own token budget and timeout, optional parallel fan-out, and returns a structured result to the parent -without polluting the parent's context — with live visibility of the subagent tree in the UI and the ability to +without polluting the parent's context, with live visibility of the subagent tree in the UI and the ability to cancel a branch. ## Why @@ -24,7 +24,7 @@ Zero-context-cost delegation, done with OTP processes and message passing rather - Context isolation: child gets persona + brief + explicitly passed context snippets only. - UI: subagent tree panel in the session view (status, tokens, cancel); child sessions browsable. **Out:** -- Cross-node subagents (distributed Erlang) — noted as a follow-up; design keeps pids opaque. +- Cross-node subagents (distributed Erlang): noted as a follow-up; design keeps pids opaque. ## Acceptance criteria 1. [auto] Parent delegates a brief; child completes with FakeProvider; parent receives a `tool` message containing the child's structured result; parent's history does not include the child's messages (test). @@ -38,10 +38,10 @@ Zero-context-cost delegation, done with OTP processes and message passing rather ## Manual verification queue Every `[manual]` criterion below needs a person. Listed here so the owner sees the queue at G1 rather than at review time. -- **AC7** — UI screenshot of the tree during a run. +- **AC7**: UI screenshot of the tree during a run. ## Definition of Done - [ ] gate green · [ ] AC1–7 proven · [ ] docs/01 synced · [ ] ROADMAP → done · [ ] commit + tag ## Commit & tag -`feat(s080): complete slice 080 — subagents and delegation` · tag `slice/080` +`feat(s080): complete slice 080 (subagents and delegation)` · tag `slice/080` diff --git a/slices/081-a2a-agent-card/SLICE.md b/slices/081-a2a-agent-card/SLICE.md index 8a9af1d..c9f60ee 100644 --- a/slices/081-a2a-agent-card/SLICE.md +++ b/slices/081-a2a-agent-card/SLICE.md @@ -1,9 +1,9 @@ -# Slice 081 — A2A v1.0 Agent Card and task intake (optional) +# Slice 081: A2A v1.0 Agent Card and task intake (optional) | Field | Value | |---|---| | Phase | 8 Orchestration | -| Milestone | — (optional, post-M6) | +| Milestone | none (optional, post-M6) | | Size | M | | Depends on | 080, 061 | @@ -41,4 +41,4 @@ If that changes during the slice, the criterion is retagged and this section is - [ ] `mix gate` green · [ ] AC1–3 proven · [ ] docs/ADR/VERSIONS updated if affected · [ ] ROADMAP status → done · [ ] final commit + tag ## Commit & tag -`feat(s081): complete slice 081 — A2A agent card` · tag `slice/081` +`feat(s081): complete slice 081 (A2A agent card)` · tag `slice/081` diff --git a/slices/090-observability-cost/SLICE.md b/slices/090-observability-cost/SLICE.md index 53936f2..90cd9bc 100644 --- a/slices/090-observability-cost/SLICE.md +++ b/slices/090-observability-cost/SLICE.md @@ -1,4 +1,4 @@ -# Slice 090 — Observability: telemetry, cost ledger, LiveDashboard +# Slice 090: Observability: telemetry, cost ledger, LiveDashboard | Field | Value | |---|---| @@ -15,7 +15,7 @@ structured logs with redaction; optional OpenTelemetry export. ## Scope **In:** - `Trinity.Telemetry` events catalogue documented in `docs/telemetry.md`; `:telemetry` handlers → `usage_events`, metrics (Telemetry.Metrics), and a ring buffer for the Activity page. -- Cost ledger: `Trinity.Telemetry.Costs` — totals by day/session/persona/model from `usage_events`; budgets in settings; when exceeded: warn in UI, optionally block new turns (setting). +- Cost ledger: `Trinity.Telemetry.Costs`: totals by day/session/persona/model from `usage_events`; budgets in settings; when exceeded: warn in UI, optionally block new turns (setting). - LiveDashboard mounted (dev always; prod behind setting) with custom pages: sessions (pids, state, memory), tools latency, LLM latency/tokens. - Activity page: recent events stream, filter by session/type. - Log redaction: a `Logger` filter that masks API keys and truncates prompts at `:info`. @@ -33,12 +33,12 @@ structured logs with redaction; optional OpenTelemetry export. ## Manual verification queue Every `[manual]` criterion below needs a person. Listed here so the owner sees the queue at G1 rather than at review time. -- **AC2** — Cost totals match the sum of `usage_events` for a seeded dataset; budget exceeded triggers the warning event and, when set, blocks a new turn with…. -- **AC3** — LiveDashboard custom page lists live sessions with their gen_statem state (screenshot). -- **AC6** — Activity page screenshot. +- **AC2**: Cost totals match the sum of `usage_events` for a seeded dataset; budget exceeded triggers the warning event and, when set, blocks a new turn with…. +- **AC3**: LiveDashboard custom page lists live sessions with their gen_statem state (screenshot). +- **AC6**: Activity page screenshot. ## Definition of Done - [ ] gate green · [ ] AC1–6 proven · [ ] docs/telemetry.md · [ ] ROADMAP → done · [ ] commit + tag ## Commit & tag -`feat(s090): complete slice 090 — observability and cost ledger` · tag `slice/090` +`feat(s090): complete slice 090 (observability and cost ledger)` · tag `slice/090` diff --git a/slices/100-desktop-shell/SLICE.md b/slices/100-desktop-shell/SLICE.md index 700c25d..04899ea 100644 --- a/slices/100-desktop-shell/SLICE.md +++ b/slices/100-desktop-shell/SLICE.md @@ -1,4 +1,4 @@ -# Slice 100 — Desktop shell: native window, tray, notifications, keychain +# Slice 100: Desktop shell: native window, tray, notifications, keychain | Field | Value | |---|---| @@ -52,21 +52,21 @@ the OS keychain, launch-at-login, single-instance behaviour, and graceful shutdo ## Manual verification queue Every `[manual]` criterion below needs a person. Listed here so the owner sees the queue at G1 rather than at review time. -- **AC1** — Packaged build launches to the chat window with no dev tooling on the machine (fresh user account or VM): macOS + Windows screenshots (Linux if…. -- **AC2** — Tray menu actions work (screenshots); pending-approval count updates live. -- **AC3** — An approval requested while the window is hidden produces an OS notification; clicking it focuses the window on the approval card (GIF). -- **AC4** — Global hotkey shows/hides the window (GIF). -- **AC5** — Keychain: a provider key entered in Settings is retrievable after restart and absent from the DB file (`strings trinity.db | grep` returns…. -- **AC7** — Quit during a streaming turn → draft persisted as interrupted; on relaunch the banner shows (manual). -- **AC8** — Second launch focuses the first instance (manual). -- **AC11** — First launch with no configuration reaches a working first turn through the setup path, on a fresh account (manual, screenshots). +- **AC1**: Packaged build launches to the chat window with no dev tooling on the machine (fresh user account or VM): macOS + Windows screenshots (Linux if…. +- **AC2**: Tray menu actions work (screenshots); pending-approval count updates live. +- **AC3**: An approval requested while the window is hidden produces an OS notification; clicking it focuses the window on the approval card (GIF). +- **AC4**: Global hotkey shows/hides the window (GIF). +- **AC5**: Keychain: a provider key entered in Settings is retrievable after restart and absent from the DB file (`strings trinity.db | grep` returns…. +- **AC7**: Quit during a streaming turn → draft persisted as interrupted; on relaunch the banner shows (manual). +- **AC8**: Second launch focuses the first instance (manual). +- **AC11**: First launch with no configuration reaches a working first turn through the setup path, on a fresh account (manual, screenshots). ## Definition of Done - [ ] gate green · [ ] AC1–11 proven · [ ] docs/packaging.md, docs/07 synced · [ ] ADR-0004 status accepted · [ ] ROADMAP → done · [ ] commit + tag ## Commit & tag -`feat(s100): complete slice 100 — desktop shell` · tag `slice/100` +`feat(s100): complete slice 100 (desktop shell)` · tag `slice/100` ## Risks / open questions - Keychain access from the BEAM: prefer the Tauri bridge (Rust side does the OS work) so no NIF is needed. -- Windows: if the ex_tauri path was replaced in 001, the bridge protocol must be reimplemented in the chosen shell — budget time. +- Windows: if the ex_tauri path was replaced in 001, the bridge protocol must be reimplemented in the chosen shell; budget time. diff --git a/slices/101-release-pipeline/SLICE.md b/slices/101-release-pipeline/SLICE.md index e5ee619..4b61ddc 100644 --- a/slices/101-release-pipeline/SLICE.md +++ b/slices/101-release-pipeline/SLICE.md @@ -1,4 +1,4 @@ -# Slice 101 — Release pipeline: signing, notarization, auto-update +# Slice 101: Release pipeline: signing, notarization, auto-update | Field | Value | |---|---| @@ -10,7 +10,7 @@ ## Goal A CI release workflow that builds, signs, notarizes (macOS), signs (Windows), packages installers (`.dmg`/`.app`, `.msi`/`.exe`, `.AppImage`/`.deb`), publishes to GitHub Releases with an updater manifest, and an in-app updater -that checks, downloads, verifies, and applies updates — with migrations run safely on first launch of a new +that checks, downloads, verifies, and applies updates, with migrations run safely on first launch of a new version and a rollback story. ## Why @@ -23,7 +23,7 @@ effective March 2026) at M0. - `mix trinity.release` orchestrating Burrito targets + the shell's bundler; version from `mix.exs` + git tag. - CI (GitHub Actions) matrix: macos-latest (arm64 + x86_64 or universal), windows-latest, ubuntu-latest; secrets for certs; artifact upload; release notes from conventional commits. - macOS: Developer ID signing, hardened runtime, entitlements (network client, file access as needed), notarization via `notarytool`, stapling; Gatekeeper check documented. -- Windows: Authenticode signing via cloud/HSM signer (Azure Trusted Signing or vendor tool) — the pipeline supports a "sign step" that the human configures; SmartScreen note. +- Windows: Authenticode signing via cloud/HSM signer (Azure Trusted Signing or vendor tool); the pipeline supports a "sign step" that the human configures; SmartScreen note. - Updater: Tauri updater plugin (if ex_tauri path) with signed manifest; otherwise a minimal in-app updater (download, verify signature/sha, swap, relaunch; Windows rename-running-exe trick). Update channel setting (stable/beta). Check on launch + daily. - Data safety: backup the SQLite file before running migrations on a new version; keep last 3 backups; migration failure → restore + show error. - `docs/release.md`: full runbook. @@ -32,7 +32,7 @@ effective March 2026) at M0. ## Acceptance criteria 1. [auto] CI release run on a tag produces artifacts for each target (run URL/log). 2. [manual] macOS: `spctl --assess --type execute` and `stapler validate` pass on the produced app (output). -3. [manual] Windows: `signtool verify /pa` passes (output) — or documented "unsigned pending cert" with the pipeline step proven using a self-signed cert in a test run. +3. [manual] Windows: `signtool verify /pa` passes (output), or documented "unsigned pending cert" with the pipeline step proven using a self-signed cert in a test run. 4. [manual] Updater: install version N, publish N+1 to a test channel, app detects, downloads, verifies, updates, relaunches on N+1 (GIF/screenshots per OS). 5. [manual] Migration backup: simulate a failing migration on update → DB restored, error shown, app still opens on N (manual with a deliberately broken migration in a test branch). 6. [auto] Release notes generated from commits since last tag (excerpt). @@ -41,17 +41,17 @@ effective March 2026) at M0. ## Manual verification queue Every `[manual]` criterion below needs a person. Listed here so the owner sees the queue at G1 rather than at review time. -- **AC2** — macOS: `spctl --assess --type execute` and `stapler validate` pass on the produced app (output). -- **AC3** — Windows: `signtool verify /pa` passes (output) — or documented "unsigned pending cert" with the pipeline step proven using a self-signed cert in a…. -- **AC4** — Updater: install version N, publish N+1 to a test channel, app detects, downloads, verifies, updates, relaunches on N+1 (GIF/screenshots per OS). -- **AC5** — Migration backup: simulate a failing migration on update → DB restored, error shown, app still opens on N (manual with a deliberately broken…. +- **AC2**: macOS: `spctl --assess --type execute` and `stapler validate` pass on the produced app (output). +- **AC3**: Windows: `signtool verify /pa` passes (output), or documented "unsigned pending cert" with the pipeline step proven using a self-signed cert in a…. +- **AC4**: Updater: install version N, publish N+1 to a test channel, app detects, downloads, verifies, updates, relaunches on N+1 (GIF/screenshots per OS). +- **AC5**: Migration backup: simulate a failing migration on update → DB restored, error shown, app still opens on N (manual with a deliberately broken…. ## Definition of Done - [ ] gate green · [ ] AC1–7 proven (cert-dependent ACs may be conditionally waived by the human with a follow-up slice) · [ ] docs/release.md · [ ] ROADMAP → done · [ ] commit + tag ## Commit & tag -`feat(s101): complete slice 101 — release pipeline` · tag `slice/101` (and the app's first `v0.1.0` tag) +`feat(s101): complete slice 101 (release pipeline)` · tag `slice/101` (and the app's first `v0.1.0` tag) ## Risks / open questions - Notarization can take minutes to hours; the workflow must poll, not assume. -- Universal macOS binary vs two artifacts — decide by Burrito/Tauri support at the time. +- Universal macOS binary vs two artifacts: decide by Burrito/Tauri support at the time. diff --git a/slices/110-luerl-sandbox/SLICE.md b/slices/110-luerl-sandbox/SLICE.md index d3a84e6..a22046b 100644 --- a/slices/110-luerl-sandbox/SLICE.md +++ b/slices/110-luerl-sandbox/SLICE.md @@ -1,4 +1,4 @@ -# Slice 110 — Luerl sandbox + executable skills +# Slice 110: Luerl sandbox + executable skills | Field | Value | |---|---| @@ -20,10 +20,10 @@ Skills that can execute need a real programming surface. This gives the agent on **In:** - `Trinity.Sandbox` context: `run(code, opts)` → `{:ok, result, stats} | {:error, reason}`; `Trinity.Sandbox.Runner` pool under `Trinity.Sandbox.Supervisor` (poolboy or a simple DynamicSupervisor with a cap); per-run process with `max_heap_size`, reduction limit via `luerl_sandbox`, wall-clock timeout, output size cap. - Host API exposed to Lua: `trinity.tool(name, args)` (goes through the permission gate as the calling session), `trinity.memory.recall(q, k)`, `trinity.log(msg)`, `trinity.result(table)`, `json.encode/decode`, string/table/math stdlib subsets. No `os`, `io`, `require`, `load`, `dofile`, `package`. -- `run_lua` tool (risk `:exec`, but since it is in-VM, default policy `:ask` first time then "allow for session" — configurable); skill scripts: `skill_run(name, args)` executes `scripts/<lua_entry>` with the skill's declared `requires_tools` pre-checked. +- `run_lua` tool (risk `:exec`, but since it is in-VM, default policy `:ask` first time then "allow for session"; configurable); skill scripts: `skill_run(name, args)` executes `scripts/<lua_entry>` with the skill's declared `requires_tools` pre-checked. - Scanner (041) extended to Lua: flags attempts to reference forbidden globals. - UI: sandbox runs visible in the tool-call card with stats (reductions, time, memory). -- Docs: `docs/sandbox.md` — capabilities, limits, threat model, what it does not protect against (native/shell). +- Docs: `docs/sandbox.md`: capabilities, limits, threat model, what it does not protect against (native/shell). **Out:** WebAssembly runtime (interesting future option), Python. ## Acceptance criteria @@ -39,15 +39,15 @@ Skills that can execute need a real programming surface. This gives the agent on ## Manual verification queue Every `[manual]` criterion below needs a person. Listed here so the owner sees the queue at G1 rather than at review time. -- **AC1** — Infinite loop script → terminated by reduction limit within the configured bound; runner process gone; no VM impact (test measuring scheduler…. -- **AC7** — Manual: ask the agent to "compute the total size of all markdown files under X using a script" → it writes Lua, runs it via the sandbox, returns…. -- **AC8** — `docs/sandbox.md` reviewed by the human. +- **AC1**: Infinite loop script → terminated by reduction limit within the configured bound; runner process gone; no VM impact (test measuring scheduler…. +- **AC7**: Manual: ask the agent to "compute the total size of all markdown files under X using a script" → it writes Lua, runs it via the sandbox, returns…. +- **AC8**: `docs/sandbox.md` reviewed by the human. ## Definition of Done - [ ] gate green · [ ] AC1–8 proven · [ ] docs/07, docs/sandbox.md · [ ] VERSIONS (luerl/sandbox ✅) · [ ] ROADMAP → done · [ ] commit + tag ## Commit & tag -`feat(s110): complete slice 110 — Luerl sandbox and executable skills` · tag `slice/110` +`feat(s110): complete slice 110 (Luerl sandbox and executable skills)` · tag `slice/110` ## Risks / open questions -- Luerl performance for data-heavy scripts; measure and document (it is a sandbox, not a runtime for heavy compute — heavy work goes to approved shell tools). +- Luerl performance for data-heavy scripts; measure and document (it is a sandbox, not a runtime for heavy compute: heavy work goes to approved shell tools). diff --git a/slices/120-oss-hygiene-governance/SLICE.md b/slices/120-oss-hygiene-governance/SLICE.md index 8e308f6..ac04460 100644 --- a/slices/120-oss-hygiene-governance/SLICE.md +++ b/slices/120-oss-hygiene-governance/SLICE.md @@ -1,4 +1,4 @@ -# Slice 120 — Open-source hygiene and governance, audited +# Slice 120: Open-source hygiene and governance, audited | Field | Value | |---|---| @@ -47,4 +47,4 @@ If that changes during the slice, the criterion is retagged and this section is - [ ] `mix gate` green · [ ] AC1–4 proven · [ ] docs/ADR/VERSIONS updated if affected · [ ] ROADMAP status → done · [ ] final commit + tag ## Commit & tag -`chore(s120): complete slice 120 — OSS hygiene and governance audit` · tag `slice/120` +`chore(s120): complete slice 120 (OSS hygiene and governance audit)` · tag `slice/120` diff --git a/slices/121-supply-chain-provenance/SLICE.md b/slices/121-supply-chain-provenance/SLICE.md index 258c151..3cb0011 100644 --- a/slices/121-supply-chain-provenance/SLICE.md +++ b/slices/121-supply-chain-provenance/SLICE.md @@ -1,4 +1,4 @@ -# Slice 121 — Supply chain: SBOM, signed releases, provenance, Scorecard, MCP Registry entry +# Slice 121: Supply chain: SBOM, signed releases, provenance, Scorecard, MCP Registry entry | Field | Value | |---|---| @@ -40,10 +40,10 @@ Practices badge application, and a `server.json` for the MCP Registry describing ## Manual verification queue Every `[manual]` criterion below needs a person. Listed here so the owner sees the queue at G1 rather than at review time. -- **AC4** — `server.json` validates against the MCP Registry schema (command); publication itself is an owner action. +- **AC4**: `server.json` validates against the MCP Registry schema (command); publication itself is an owner action. ## Definition of Done - [ ] `mix gate` green · [ ] AC1–5 proven · [ ] docs/ADR/VERSIONS updated if affected · [ ] ROADMAP status → done · [ ] final commit + tag ## Commit & tag -`feat(s121): complete slice 121 — supply chain and provenance` · tag `slice/121` +`feat(s121): complete slice 121 (supply chain and provenance)` · tag `slice/121` diff --git a/slices/122-aaif-sandbox-proposal/SLICE.md b/slices/122-aaif-sandbox-proposal/SLICE.md index d17570b..cb11535 100644 --- a/slices/122-aaif-sandbox-proposal/SLICE.md +++ b/slices/122-aaif-sandbox-proposal/SLICE.md @@ -1,4 +1,4 @@ -# Slice 122 — Foundation Sandbox proposal package (owner-gated) +# Slice 122: Foundation Sandbox proposal package (owner-gated) | Field | Value | |---|---| @@ -50,11 +50,11 @@ line ready to insert on acceptance. ## Manual verification queue Every `[manual]` criterion below needs a person. Listed here so the owner sees the queue at G1 rather than at review time. -- **AC2** — The goose interop proof (screenshots + commands) is in `proof/`. -- **AC4** — Filing is an owner action; this slice is done when the package is complete, not when it is filed. +- **AC2**: The goose interop proof (screenshots + commands) is in `proof/`. +- **AC4**: Filing is an owner action; this slice is done when the package is complete, not when it is filed. ## Definition of Done - [ ] `mix gate` green · [ ] AC1–4 proven · [ ] docs/ADR/VERSIONS updated if affected · [ ] ROADMAP status → done · [ ] final commit + tag ## Commit & tag -`docs(s122): complete slice 122 — AAIF Sandbox proposal package` · tag `slice/122` +`docs(s122): complete slice 122 (AAIF Sandbox proposal package)` · tag `slice/122` diff --git a/slices/123-shared-libraries-extraction/SLICE.md b/slices/123-shared-libraries-extraction/SLICE.md index 9cd37a4..70e4455 100644 --- a/slices/123-shared-libraries-extraction/SLICE.md +++ b/slices/123-shared-libraries-extraction/SLICE.md @@ -1,4 +1,4 @@ -# Slice 123 — Extract the shared components as Hex packages +# Slice 123: Extract the shared components as Hex packages | Field | Value | |---|---| @@ -52,4 +52,4 @@ If that changes during the slice, the criterion is retagged and this section is need revisiting against the naming policy before anything is published. ## Commit & tag -`feat(s123): complete slice 123 — shared libraries extracted` · tag `slice/123` +`feat(s123): complete slice 123 (shared libraries extracted)` · tag `slice/123` diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index c32d7ea..7ca49f1 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -13,7 +13,7 @@ use std::time::Duration; // Flipped to false when the app is quitting. The channel threads stop sending // heartbeats once this is false, which lets the sidecar detect heartbeat loss -// and shut itself down gracefully — the only graceful path on Windows, where +// and shut itself down gracefully: the only graceful path on Windows, where // there is no SIGTERM to deliver. static HEARTBEAT_ACTIVE: AtomicBool = AtomicBool::new(true); @@ -104,7 +104,7 @@ fn kill_sidecar(app: &tauri::AppHandle) { { // No SIGTERM on Windows. The heartbeat was stopped above, // so the sidecar's ShutdownManager times out (1500ms by - // default) and exits gracefully on its own — give it time + // default) and exits gracefully on its own; give it time // to do so before falling through to the hard kill. std::thread::sleep(Duration::from_millis(2000)); } @@ -225,7 +225,7 @@ fn resolve_port() -> u16 { } // Phoenix releases sign session cookies with SECRET_KEY_BASE. Respect one if -// provided; otherwise generate a per-launch secret — sessions reset between +// provided; otherwise generate a per-launch secret: sessions reset between // launches, which is fine for a local desktop app. fn secret_key_base() -> String { if let Ok(secret) = std::env::var("SECRET_KEY_BASE") { @@ -263,7 +263,7 @@ fn secret_key_base() -> String { fn start_server(app: &tauri::AppHandle, port: u16) { // PORT and SECRET_KEY_BASE are always injected: every server needs a port, // and SECRET_KEY_BASE is a random per-launch secret (inert if unused). The - // remaining pairs come from `config :ex_tauri, :sidecar_env` — the Phoenix + // remaining pairs come from `config :ex_tauri, :sidecar_env`: the Phoenix // defaults (PHX_SERVER/PHX_HOST) unless overridden for another framework. let env: std::collections::HashMap<String, String> = std::collections::HashMap::from([ ("PORT".to_string(), port.to_string()), @@ -321,7 +321,7 @@ fn check_server_started(port: u16) { } // Points the window at the port actually in use. When the OS assigned a free -// port (production), the compile-time URL in tauri.conf.json is wrong — and +// port (production), the compile-time URL in tauri.conf.json is wrong, and // even in dev this reload recovers the webview if it raced the server boot. fn navigate_main_window(app: &tauri::AppHandle, port: u16) { if let Some(window) = app.get_webview_window("main") { @@ -334,7 +334,7 @@ fn navigate_main_window(app: &tauri::AppHandle, port: u16) { // The sidecar channel carries heartbeats (liveness), commands from Elixir // (ExTauri.Desktop: notifications, tray, ...), and native events back to -// Elixir — all as newline-delimited JSON over the ShutdownManager socket. +// Elixir: all as newline-delimited JSON over the ShutdownManager socket. fn start_channel(app: tauri::AppHandle) { println!("Starting sidecar channel (heartbeat + desktop commands)..."); @@ -343,7 +343,7 @@ fn start_channel(app: tauri::AppHandle) { // Outer loop: (re)establish the connection. The sidecar's listener can // come up late (slow boot) or be recreated, so a dropped connection must - // reconnect rather than end the heartbeat — otherwise the backend would + // reconnect rather than end the heartbeat: otherwise the backend would // see the heartbeat stop and shut itself down. Everything exits once // HEARTBEAT_ACTIVE is cleared (the app is quitting): stopping the // heartbeat is what tells the sidecar to shut down gracefully. @@ -390,7 +390,7 @@ fn start_channel(app: tauri::AppHandle) { } // Writer (this thread): drain the queue onto the socket. A failed - // write means the connection dropped — clean up and reconnect. + // write means the connection dropped; clean up and reconnect. let mut stream = stream; for message in rx.iter() { if writeln!(stream, "{}", message).is_err() { diff --git a/templates/ADR-TEMPLATE.md b/templates/ADR-TEMPLATE.md index 1d76ac7..4cb2176 100644 --- a/templates/ADR-TEMPLATE.md +++ b/templates/ADR-TEMPLATE.md @@ -1,4 +1,4 @@ -# ADR-NNNN — <Title> +# ADR-NNNN: <Title> Status: proposed | accepted | superseded by ADR-XXXX · Date: YYYY-MM-DD ## Context diff --git a/templates/PROOF-TEMPLATE.md b/templates/PROOF-TEMPLATE.md index 10bab8b..e1520e1 100644 --- a/templates/PROOF-TEMPLATE.md +++ b/templates/PROOF-TEMPLATE.md @@ -1,4 +1,4 @@ -# PROOF — Slice NNN — <Title> +# Proof for slice NNN: <Title> Agent: <model/version> · Date: YYYY-MM-DD · Branch: slice/NNN-… · Final commit: <sha> @@ -8,7 +8,7 @@ What was built, what was hard, what was deferred (link NOTES.md follow-ups). ## Gate ``` $ mix gate -<trimmed output — must end in success> +<trimmed output; must end in success> ``` ## Tests @@ -19,14 +19,14 @@ $ mix test --cover ## Acceptance criteria evidence -### AC1 — <text of criterion> +### AC1: <text of criterion> ``` $ <command> <output> ``` Notes: … -### AC2 — … +### AC2: ... ## Manual verification for the reviewer (if anything cannot be proven in CI) Steps the human should run, expected result. diff --git a/templates/SLICE-TEMPLATE.md b/templates/SLICE-TEMPLATE.md index 8997340..5c007c9 100644 --- a/templates/SLICE-TEMPLATE.md +++ b/templates/SLICE-TEMPLATE.md @@ -1,4 +1,4 @@ -# Slice NNN — <Title> +# Slice NNN: <Title> | Field | Value | |---|---| @@ -50,7 +50,7 @@ at review time. One line each: what they do, and what a pass looks like. - [ ] Final commit + tag ## Commit & tag -`feat(sNNN): complete slice NNN — <title>` · tag `slice/NNN` +`feat(sNNN): complete slice NNN (<title>)` · tag `slice/NNN` ## Risks / open questions - … diff --git a/test/desktop_children_test.exs b/test/desktop_children_test.exs index fd0ea0c..0bd1f8e 100644 --- a/test/desktop_children_test.exs +++ b/test/desktop_children_test.exs @@ -10,7 +10,7 @@ defmodule DesktopChildrenTest do * the dependency is available in every environment, so the packaged binary carries the heartbeat. If someone re-adds `only: :dev` the release loses its shutdown mechanism and - nothing else notices — finding F1's failure mode, shipped. + nothing else notices: finding F1's failure mode, shipped. * the child is excluded from `:test` at compile time, not by asking whether the module happens to be loaded. A `Code.ensure_loaded?/1` guard returns the same empty list whether the exclusion was intended or the dependency vanished. diff --git a/test/gate_alias_test.exs b/test/gate_alias_test.exs index 26214ec..577fca3 100644 --- a/test/gate_alias_test.exs +++ b/test/gate_alias_test.exs @@ -40,7 +40,7 @@ defmodule GateAliasTest do assert List.last(steps) =~ "plan_check.sh", "the gate's last step is #{inspect(List.last(steps))}. scripts/plan_check.sh runs " <> "inside `mix gate` so that a green gate cannot coexist with a failing plan " <> - "check — which happened three times in slice 001, twice reaching the remote, " <> + "check, which happened three times in slice 001, twice reaching the remote, " <> "because two commands printed two exit codes and only one was read." assert Enum.count(steps, &(&1 =~ "plan_check.sh")) == 1, diff --git a/test/mix/tasks/versions_verify_test.exs b/test/mix/tasks/versions_verify_test.exs index 030ed5c..5655a1f 100644 --- a/test/mix/tasks/versions_verify_test.exs +++ b/test/mix/tasks/versions_verify_test.exs @@ -50,7 +50,7 @@ defmodule Mix.Tasks.Versions.VerifyUndocumentedTest do @moduledoc """ The red is planted through the argument, not through `mix.exs`. Adding an unfetched dependency there makes Mix refuse to run at all, so the task never executes and the check - proves nothing — which is what happened on the first attempt. + proves nothing, which is what happened on the first attempt. """ test "RED: a direct dependency with no row is reported" do diff --git a/test/smoke_test.exs b/test/smoke_test.exs index 97a00c1..1c250ff 100644 --- a/test/smoke_test.exs +++ b/test/smoke_test.exs @@ -6,7 +6,7 @@ defmodule SmokeTest do own exit call", so `halt` is injected and the test asserts it was called. Committed failing against a `run/2` that reports the port and returns. - The end-to-end half of this — the real binary, `ps` before and after — is AC7 and lives in + The end-to-end half of this (the real binary, `ps` before and after) is AC7 and lives in PROOF.md. A unit test cannot prove a process died; it can prove this code asked it to. """ use ExUnit.Case, async: false diff --git a/test/sobelow_skips_test.exs b/test/sobelow_skips_test.exs index 7b95861..4f7a283 100644 --- a/test/sobelow_skips_test.exs +++ b/test/sobelow_skips_test.exs @@ -4,7 +4,7 @@ defmodule SobelowSkipsTest do use ExUnit.Case, async: true @moduledoc """ - Measured at slice 000: sobelow's skip file does NOT accept a trailing comment — appending one + Measured at slice 000: sobelow's skip file does NOT accept a trailing comment; appending one changes the line, the fingerprint stops matching and the finding reappears. So reasons live in `.sobelow-skips.reasons`, keyed by fingerprint, and this test makes a reasonless skip fail the gate. @@ -65,7 +65,7 @@ defmodule SobelowSkipsTest do for path <- sources(), {line, idx} <- File.read!(path) |> String.split("\n") |> Enum.with_index(), # An attribute DEFINITION, anchored at the start of the line. `String.contains?` - # matched this file's own moduledoc and assertion strings on the first run — the + # matched this file's own moduledoc and assertion strings on the first run: the # check was wrong about what a skip is, so the check is what changed. Regex.match?(~r/^\s*@sobelow_skip\s+\[/, line), do: {path, idx} diff --git a/test/support/network_guard.ex b/test/support/network_guard.ex index e335c9b..66beb53 100644 --- a/test/support/network_guard.ex +++ b/test/support/network_guard.ex @@ -5,7 +5,7 @@ defmodule Trinity.NetworkGuard do CLAUDE.md §5: "Tests must not hit the network." The block applies to the **default** test run only. `docs/03-conventions.md` and CLAUDE.md §5 - both define an opt-in path — `@tag :live`, run with `mix test --only live` — for tests that + both define an opt-in path (`@tag :live`, run with `mix test --only live`) for tests that exist precisely to reach a real provider. Blocking those by construction would break the path the plan defines, so the guard opens when `TRINITY_LIVE=1` is set and the gate excludes `:live`. diff --git a/test/version_form_test.exs b/test/version_form_test.exs index 927d49d..cb9a7b4 100644 --- a/test/version_form_test.exs +++ b/test/version_form_test.exs @@ -6,7 +6,7 @@ defmodule VersionFormTest do @moduledoc """ There is no exemption list. The pattern is case-sensitive on word boundaries, which is what - excludes lower-case library version strings — not a list of allowed sites. + excludes lower-case library version strings, not a list of allowed sites. """ test "the skip list holds exactly one entry: the enforcer's own source" do diff --git a/test/versions_toolchain_mark_test.exs b/test/versions_toolchain_mark_test.exs index 9cab4fc..3e4b9c3 100644 --- a/test/versions_toolchain_mark_test.exs +++ b/test/versions_toolchain_mark_test.exs @@ -9,7 +9,7 @@ defmodule VersionsToolchainMarkTest do | `Rust + Tauri CLI` | stable | ✅ `.tool-versions` | ... | while `grep -in 'rust\\|tauri' .tool-versions` exited 1. The mark asserted a fact the file it - names does not carry — finding B3's defect, in the enforcer built to prevent it. + names does not carry: finding B3's defect, in the enforcer built to prevent it. These tests fail at that sha. The fix makes each toolchain row state its own derivation source and derives the mark from it.