Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .credo.exs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
requires: [],
checks: %{
extra: [
# CLAUDE.md §5: no runtime evaluation of model output. Covers the whole family,
# docs/03-conventions.md, Engineering rules: no runtime evaluation of model output.
# Covers the whole family,
# because naming one function leaves an obvious bypass.
{Trinity.Credo.NoEvalOnModelOutput, []}
],
Expand Down
9 changes: 4 additions & 5 deletions .githooks/commit-msg
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
# because a trailer that reaches a tag is permanent (CLAUDE.md §4).
#
# Enable with: git config core.hooksPath .githooks
# Enforced by: scripts/plan_check.sh rule 8, which checks the history itself,
# so a bypassed or unconfigured hook still fails the gate.
# Enforced by: the gate's DCO check in CI, which reads the history itself, so a
# bypassed or unconfigured hook still fails the build.
set -eu
msg_file="$1"
tmp="$msg_file.stripped"
Expand All @@ -24,9 +24,8 @@ rm -f "$tmp"
# Warning only: does every repo path the message names appear in the staged diff?
#
# Added at slice 001 G4, for a mistake this slice made three times: a commit whose message ran
# ahead of its tree. Twice it was an unread `plan_check exit=1`; once a message and a NOTES
# line said `src-tauri/Cargo.lock` had been added to REUSE.toml when the edit had failed on a
# stale anchor, in the same output as a green gate. `mix gate` does not check that a commit
# ahead of its tree: a message said `src-tauri/Cargo.lock` had been added to REUSE.toml
# when the edit had failed on a stale anchor, in the same output as a green gate. `mix gate` does not check that a commit
# message is true, and nothing else did either.
#
# **It warns and never blocks**, deliberately. A message legitimately names paths it does not
Expand Down
2 changes: 1 addition & 1 deletion .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
#
# Dependabot opens a pull request only when a dependency has a security advisory. Version
# bumps are not raised: 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). On its first run, with
# proposed and reviewed, never adopted silently (docs/03-conventions.md). On its first run, with
# the limit above zero, it opened five version-bump pull requests inside two minutes; one of
# them was merged by mistake. An open-pull-requests-limit of 0 keeps the security updates and
# drops the rest. The value here is a security alert arriving as a diff with the gate run
Expand Down
55 changes: 39 additions & 16 deletions .github/workflows/gate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,12 @@ jobs:
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
fetch-depth: 0 # plan_check rule 8 reads the whole history
fetch-depth: 0 # the DCO check below reads the whole history
# 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.
# authored by nobody and signed off by nobody, which the DCO check below rightly
# refuses. The branch head is what was written and signed, so that is what the gate
# reads. The repository ruleset requires a 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
Expand All @@ -39,11 +38,7 @@ jobs:

- run: mix deps.get

# The plan's own consistency, checked before the code's.
- run: ./scripts/plan_check.sh

# DCO. plan_check rule 8 also asserts every commit is signed off; this fails earlier
# and more legibly on a pull request.
# Developer Certificate of Origin: every commit carries a sign-off (ADR-0012).
- name: DCO sign-off present on every commit
run: |
git log --format=%B ${{ github.event.pull_request.base.sha || 'HEAD~1' }}..HEAD 2>/dev/null \
Expand Down Expand Up @@ -120,6 +115,37 @@ jobs:
- id: tag
run: echo "ref=$(scripts/fips_image_tag.sh --ref)" >> "$GITHUB_OUTPUT"

# The image tag is a hash of the files that define the image, so any change to them -
# including a comment - names an image that has to be built before it can be pulled. The
# `fips` job below takes that image as its container, which GitHub pulls before the job's
# first step runs, so the job cannot retry the pull itself: it fails with `manifest
# unknown` and no output. The fips-image workflow builds and pushes on the same event, so
# the two race, and the loser is always this one.
#
# Waiting here turns that race into a wait. The image is a function of its inputs, so the
# one being waited for is exactly the one this commit needs, and a build from source is
# slow enough that the ceiling is generous. A timeout fails with the reason rather than
# with a pull error, because "the image is still building" and "the image will never
# exist" need different responses from whoever reads it.
- name: Wait for the FIPS image to be published
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
ref='${{ steps.tag.outputs.ref }}'
echo "$GH_TOKEN" | docker login ghcr.io -u '${{ github.actor }}' --password-stdin
for attempt in $(seq 1 60); do
if docker manifest inspect "$ref" >/dev/null 2>&1; then
echo "$ref is published (attempt $attempt)"
exit 0
fi
echo "attempt $attempt: $ref is not published yet; waiting 30s"
sleep 30
done
echo "::error::$ref was not published within 30 minutes. It is built by the" \
"fips-image workflow, which runs when ci/fips/Containerfile, .tool-versions or" \
"scripts/fips_image_tag.sh changes. Check that workflow's run for this commit."
exit 1

fips:
needs: fips-tag
runs-on: ubuntu-latest
Expand All @@ -146,10 +172,9 @@ jobs:
ref: ${{ github.event.pull_request.head.sha || github.sha }}

# The checkout action marks the workspace safe under a temporary HOME it removes when
# it is done; every later git call in the container (plan_check, the names, secrets and
# it is done; every later git call in the container (the names, secrets and
# reuse censuses, all of which read `git ls-files`) then hits git's dubious-ownership
# refusal and reads an empty tree. Found on run 35537462271: plan_check rule 4 named
# every slice as missing its SLICE.md.
# refusal and reads an empty tree, which makes every census pass over nothing.
- run: git config --global --add safe.directory "$GITHUB_WORKSPACE"

- uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
Expand Down Expand Up @@ -185,8 +210,6 @@ jobs:
ERL_AFLAGS: "-eval application:load(crypto)"
run: elixir scripts/crypto_supports.exs | diff - docs/fips-leg/supports-default.txt

- run: ./scripts/plan_check.sh

- name: DCO sign-off present on every commit
run: |
git log --format=%B ${{ github.event.pull_request.base.sha || 'HEAD~1' }}..HEAD 2>/dev/null \
Expand Down
63 changes: 63 additions & 0 deletions .github/workflows/scorecard.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# SPDX-FileCopyrightText: Sudo Apt Holdings LLC
# SPDX-License-Identifier: Apache-2.0
#
# OpenSSF Scorecard. It measures this repository against the OpenSSF's own checks - branch
# protection, pinned dependencies, signed releases, dangerous workflow patterns, token
# permissions, maintenance and the rest - and publishes the result so that anyone evaluating the
# project can read it without being asked to trust a claim made here.
#
# It runs weekly rather than on every push because the checks are about the repository's
# configuration and history, which change slowly, and because the analysis reads the GitHub API
# rather than the tree. The results are uploaded to the code-scanning dashboard, so a regression
# in the repository's posture is visible in the same place as a code finding.
name: scorecard

on:
branch_protection_rule:
schedule:
# Mondays, 07:00 UTC. A fixed day makes a drop in the score attributable to the week it
# appeared in rather than to an unknown point since the last look.
- cron: '0 7 * * 1'
push:
branches: [main]

permissions: read-all

jobs:
analysis:
name: Scorecard analysis
runs-on: ubuntu-latest
permissions:
# Upload the result to the code-scanning dashboard.
security-events: write
# Read the repository's own metadata (branch protection, releases, workflow runs).
id-token: write
contents: read
actions: read

steps:
- name: Checkout
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
persist-credentials: false

- name: Run analysis
uses: ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc # v2.4.4
with:
results_file: results.sarif
results_format: sarif
# Publishing makes the score readable at deps.dev and through the Scorecard API, which
# is the point: an evaluator should be able to check the posture without asking us.
publish_results: true

- name: Upload the result as an artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: scorecard-results
path: results.sarif
retention-days: 30

- name: Upload the result to code scanning
uses: github/codeql-action/upload-sarif@416ff0dea110f80f0f56f0046500dbf7420e4bb0 # codeql-bundle-v2.27.1
with:
sarif_file: results.sarif
140 changes: 140 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
<!-- SPDX-FileCopyrightText: Sudo Apt Holdings LLC -->
<!-- SPDX-License-Identifier: Apache-2.0 -->
# Changelog

Trinity is developed in reviewed units of work, each merged through a pull request with the
quality gate green and then tagged. This file records what each tag delivered and how it was
verified. It is derived from the tags themselves (`git tag -l 'slice/*'`), not written from
memory.

The project is pre-alpha: there is no supported release series yet, and tags mark reviewed
increments rather than supported versions. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
loosely; versioning is by increment, and semantic versions begin at the first supported release.

Every entry below was gated on: formatting, a compile with warnings as errors, the architectural
boundary check, Credo (strict), Sobelow, dependency and licence audits, the full test suite and a
coverage floor. Entries from 2026-09-22 onward also required a release build check. Verification
evidence for each increment is retained by the maintainers and summarised here.

## Unreleased

- Repository restructured for public review: internal planning material moved out of the published
tree, engineering rules consolidated into `docs/03-conventions.md`, and this changelog added as
the public record of delivered work.

## 2026-09-23

### `slice/070` — Gateway core
Trinity becomes reachable from channels other than the desktop. An adapter behaviour carries text
and nothing else; a router admits the sender, applies a rate limit, dispatches commands and binds
the conversation to a session, streaming the reply back. An unknown sender receives a pairing code
and nothing else: no session is created and no model is called until they are paired. Approvals
raised during a channel-originated turn are rendered into that conversation, and what a channel may
approve is capped below what the local desktop may, with the refusal recorded in the receipt chain.
Ships an in-process console adapter; messaging platforms follow.

## 2026-09-22

### `slice/062` — MCP authorization
OAuth 2.1 in every role for the Model Context Protocol surface: a resource server validating
audience-bound tokens against an external authorization server, an optional personal-profile
authorization server for a single operator's own machine, and a client role that obtains tokens
from protected servers. Identity is separated from authority throughout: a token establishes who is
calling, while the permission gate and the selected authority adapter decide whether an effect may
happen. No token material reaches model context, logs or receipts.

### `slice/061` — MCP server
Trinity serves the Model Context Protocol at revision 2026-07-28 over Streamable HTTP and stdio,
with compatibility for 2025-11-25. Calls arriving over the protocol pass the same permission gate
and effect membrane as local ones and leave the same receipts. A call requiring approval is held
with sealed, tamper-evident state that the client retries once the operator has decided.

### `slice/050` — Scheduler
Scheduled agent tasks on durable jobs, surviving restart, with cron or one-shot schedules, delivery
targets, and a curator that marks stale records and archives untouched ones without deleting.

## 2026-09-21

### `slice/060` — MCP client
A thin client driver: outbound requests are built here while decoding and validation use the
protocol core's published functions. Every tool a server exposes becomes a namespaced tool that
cannot borrow a built-in tool's permissions, every result is treated as untrusted content, and a
server's mid-call question becomes an operator approval rather than an automatic answer.

### `slice/059` — MCP capability assessment
A measured gap analysis of the protocol core against the 2026-07-28 checklist, and a probe of the
extension seam the server side needs, recorded as findings rather than intentions.

### `slice/041` — Skill self-management
Skills the system proposes for itself pass through staged approval and a scanner before becoming
active.

### `slice/040` — Skills registry
A registry for the open `SKILL.md` format with progressive disclosure of instructions.

### `slice/032` — Semantic memory
Embeddings, semantic recall and hybrid retrieval, with a local model option and a vector store
behind a behaviour.

## 2026-09-20

### `slice/034` — Export, import, restore
The operator's data leaves in a documented archive format and comes back, with manifest validation.

### `slice/033` — Project context
`AGENTS.md` from a session's project roots is loaded into context with precedence and a size cap.

### `slice/031` — Session search
Full-text search over conversation history using SQLite FTS5.

### `slice/030` — Persona and always-on memory
A persona document and a memory tier always present in context, scoped session, persona and global,
with a budget and truncation recorded as evidence.

### `slice/024` — Effects, authority and receipts
The effect catalog, authority selection at boot, and locally signed receipts. Authority is an
adapter behind a behaviour: the shipped implementation decides locally, and a deployment may supply
an external one, in which case this tree keeps no executor for the effects that adapter governs.
Receipts are an Ed25519-signed hash chain with checkpoints and a verifier.

### `slice/023` — Context compaction
Conversations that outgrow the window are compacted with lineage retained.

### `slice/022` — Core tools
Filesystem, web fetch and search, and a shell tool, each behind the permission gate, with content
from outside the machine marked untrusted.

### `slice/021` — Permission gate
Every tool call is decided before it runs: allow once, for the session, or by a written rule, with
the decision and its basis receipted. Approval surfaces carry no authority of their own.

### `slice/020` — Tool protocol and registry
Tools are modules behind a behaviour plus a configuration entry; adding one requires no change to
any core module, and the registry test asserts it.

### `slice/013` — Chat interface
Streaming conversation in LiveView, with model output rendered through a single path.

### `slice/012` — Session process
One supervised process per session, with crash recovery proven by killing the process.

### `slice/011` — Model provider layer
Providers behind one behaviour, switched by configuration, with usage recorded per call.

### `slice/010` — Core domain and persistence
The data model and repository layer, SQLite first with PostgreSQL supported, with gapless ordering
under concurrent writers proven by a stress test.

### `slice/003` — FIPS build leg
A continuous integration leg that builds from source and runs the cryptographic properties inside a
FIPS-mode container, so claims about approved algorithms are measured rather than asserted.

## 2026-09-07

### `slice/001` — Packaging
A self-contained desktop binary produced for Linux, macOS and Windows, each smoke-tested in CI.

## 2026-09-06

### `slice/000` — Toolchain and quality gate
The repository, the pinned toolchain and the gate every later change has had to pass.
Loading
Loading