CHANGELOG.md
Rama main · réplica de lectura del repositorio original.
# Changelog
All notable changes to Kronn are documented in this file.
Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
Versioning: [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
Release notes for 0.9.3 and earlier are available in the
[legacy archive](docs/releases/CHANGELOG-legacy.md).
---
## [Unreleased]
### Added
- Media: an image or a video can be generated from several source images of
the discussion, ordered and removable in the Creator. Each saved image/video
model (ComfyUI included) has a reference limit in Config: empty uses what
the provider advertises, zero turns references off, a number declares an
unknown limit or lowers the advertised one. The Creator, `agent_list` and
the generation preflight read the same capability. Local ComfyUI takes
sources through SDXL img2img (1), FLUX.1 schnell + Redux (3), FLUX.2 Klein
4B distilled (4) and Wan 2.1 VACE (3), and checks the nodes and model files
each workflow needs before queueing. A source image must start like a PNG,
JPEG, WebP or GIF, and one request's sources stay under 64 MB together
(KT-1403, #265).
- A workflow Agent step on an HTTP or local agent (Ollama, LiteLLM,
OpenRouter…) can now generate an image or a video with `media_generate`
and follow the job with `media_job_status`, as in a discussion: same
validation, same per-request billing guard, same connection resolution.
The asset lands in a room opened for the run (listed with it) and, once
the job completes, as a file in the run's artifacts directory
(`asset.artifact_path`) that a later step can read. The step waits with
`wait_secs` instead of being woken. Every other task or orchestration tool
stays refused in a workflow step (KT-1605, #502).
- Reference images work in a workflow step: a picture of the run's rooms or the
project's discussions (a brand image a launch variable names), an earlier
step's generation by job id or artifact path, or a picture file inside the
project. Nothing of another run or project is reachable. A run is also capped
at 12 generations (`KRONN_WORKFLOW_MEDIA_JOBS_PER_RUN`, at most 100), on top of
six per step, and a dry-run Test no longer bills. A reference that matches
nothing gets the one scope refusal (KT-1605, #502).
### Security
- rusqlite 0.41 bundles libsqlite3-sys 0.39, past the vulnerable SQLite of
RUSTSEC-2026-0459 (KT-1620).
- The copy of a generated asset into a run's artifacts directory is written
through the directory's file descriptor, so a link an Exec step planted at
the asset's name is replaced, never written through. A press of a step's
Test button is its own scope for the media guard, never shared across
workflows or projects (KT-1605, #502).
- A project file used as a reference is read through directory fds: a link on its
path is refused, a FIFO cannot block the open, and it must be a picture by its
first bytes, not by its extension (KT-1605, #502).
- Every copy of `config.toml` (migration backup, snapshots, the set-aside of an
unreadable config, a retired key) is encrypted with the instance key, clear
copies left by earlier versions are encrypted at the next start, and the
set-aside never follows a link or replaces an existing file (KT-1528).
- Writes guarded by `fs_guard` (docs migration, templates, audits, skills,
worktrees) resolve through directory file descriptors with `O_NOFOLLOW`, so a
parent folder swapped for a link after the check never lets a write leave
the project; Windows keeps the check-then-write by path (KT-1595).
### Fixed
- Windows: Kronn could not start after an upgrade with a pending migration ("Access denied (os error 5)"), and scheduled backups failed. The backup copy was flushed through a read-only handle, which Windows refuses; it is now opened for writing first, and the Windows CI job runs the test (KT-1625, #509).
- An interrupted task execution no longer stays stuck on "recovery decision
already consumed": it can always resume or deliver, and a resume never
consumes the decision of a newer interruption (KT-1537, #492).
## [0.15.2] - 2026-11-08
### Added
- Trust levels: say "I trust my agents here" at the scope you choose, with the
risks spelled out, instead of approving every step. Three levels (Standard,
Trusted, Full access) set for the instance (Settings › Trust levels), a
workflow or a room; the narrowest explicit setting wins. Under Trusted, an
agent-written Quick Exec or workflow Exec step that passes values to a CLI
is active at once, a room's request to read or plan in another of your
projects is granted, and integration validations may reach Docker; Full
access also runs validations without the sandbox. Choosing a level opens a
risk panel listing exactly what changes and needs an explicit
confirmation; an active level shows a badge on the room, the workflow and
the settings, revocable in one click, and every approval it grants is
journalled ("granted by trust level X"). Everything a level approves runs
under an OS sandbox (macOS Seatbelt, Linux bubblewrap) that hides Kronn's
own data (database, encryption key, recovery key, configuration, backups)
from those commands and leaves the network, the API port and the rest of
your machine open, as in your terminal; a host with no sandbox refuses Trusted and Full
access with the reason. Raising a level needs a code from an authenticator
app (TOTP, any app; ten single-use recovery codes; enrolled in Settings ›
Security), so a command an agent approved cannot raise its own level, even
from this machine; lowering never needs it. Lowering a level takes back
the approvals it granted and the journal lists them. The risk panel says
what stays reachable (project databases, the network, Docker, other
repositories, the credentials of other tools) and what stays closed. The
journal keeps 10 000 rows or 180 days. No level lets an agent change a trust
setting, skip a delete confirmation, or lends its trust to a P2P peer, and a
room that edits a workflow never takes that workflow's level. Elevation
cards keep refuse / this conversation / this project and now point to the
global setting. Code write on other projects and host access for agents are
in the policy table for Full
access; their gates ship with their own tickets (KT-1492, builds on
KT-1490, KT-1474, KT-1433).
- The Page header shows the HTML weight: green up to 1 MB, orange up to 4 MB,
red above, with advice on how to optimize. The Pages list badges heavy Pages,
and a heavy write returns a notice the agent receives as `kronn_notice`
(#252).
- The review-round limit is configurable and reaches every launch path (KT-1345).
Settings > Workers sets the default (5 out of the box, or Unlimited with a
token-burn warning), readable over `GET /api/config/workers` and, for MCP
principals, as `default_max_review_rounds` in `task_exec_prepare`. The
campaign dialog is pre-filled with it, single-task MCP launches take an
optional `max_review_rounds` (integer or `"unlimited"`; before, they were
stuck on 3), and workflow-delegated subtasks use it when the step sets none.
An unknown MCP argument is now refused instead of silently ignored, and an
existing execution's limit is raised with
`task_exec_reassign({extend_review_rounds})` (or the execution card's
« Extend rounds »), which also re-offers the findings of an execution the
limit had escalated. With unlimited rounds the campaign's `token_budget`
stays authoritative: once its consumed tokens reach it, a `request_changes`
escalates with `token_budget_exhausted` instead of scheduling another
correction, a worker reassignment is refused, and an extension re-offers
nothing. See `docs/operations/worker-review-rounds.md`.
- A room can be granted `code` on another registered project, where Claude
Code edits that project's checkout like its own (KT-1491, #411). The levels
are now `read`, `plan` (what `write` always was: Kronn's planning tools,
never the files) and `code`; a stored `write` is read as `plan` (migration
285). A `code` grant never opens credentials or anything that runs later:
secrets stay closed at every level, `.git`, the hooks path, `.claude`,
`.envrc` and agent configs are denied, and a checkout that holds the Kronn
data directory is refused. `code` is enforced for Claude Code only, and
full-access agents are told so. Granting it needs the strong confirmation
when enrolled; revoking it, or lowering the trust level, stops the room's
running turn.
- Kronn checks the live database schema against its migrations at boot
(KT-1483). A missing table, index or trigger is recreated (empty), a missing
column is added back when SQLite allows it, and the rest is logged and
listed under `schema` in `GET /api/health` (`ok`, `repaired` or
`degraded`). See `docs/gotchas/schema-drift-boot-check.md`.
- The prompt limit follows the target model's context window instead of a
fixed 100 KB, under a configurable ceiling (`server.max_prompt_bytes`,
4 MB by default), on create, send and revise. The refusal names the limit
and the model and suggests attaching a file or a larger-context model
(KT-1529).
- Discussion and Artifact pages share one full-height side panel with a
vertical rail (KT-1532). Their headers share one shell: ids back next to
the title, a primary action and « Détails » pair, the Artifact details as a
side-panel tab, Sync in the Invite slot and a live « Sync the N workflows »
count (KT-1530, KT-1344). The discussion panel has a single close button
and waits instead of opening under the mobile sidebar.
- The Automation list has a « Désactivés » filter, exclusive with « Actifs »
(KT-1264).
### Changed
- An agent can now save a Quick Exec or an Exec line that passes run values to
a program Kronn does not model (`fastly purge --key {{surrogate_key}}`): it
is stored inactive, the save response says a human activates it, and the
agent carries on instead of stopping. The run keeps refusing it until then.
Shells, paths, wrappers and interpolation inside inline code are still
refused at save. A human activates it with one click on the room's card, in
« À valider », on a Page or discussion action, or on the workflow's
readiness list; the notice shows the exact line and sends exactly that line,
so a line an agent rewrote in between is refused and shown again; each argument is shown quoted, so two different argument lists never read alike; the activation checks and writes the line in one atomic step and never overwrites a concurrent edit. « Refuser »
is kept server-side (migration 277). Activation and refusal are the human's
only: a bridge token, or a request carrying the MCP bridge's `X-Kronn-Agent`
marker (a CLI launched from the host, whose room is restored before its first save after a reconnect), cannot do either, and that marker now
also keeps the other human-only routes and the Quick API access policy from
treating such a CLI as a person (KT-1490).
- Live Pages no longer refuse HTML above 1 MB. A Page may weigh what its
author wants; a 24 MiB safety guard only stops a runaway writer, and create,
update and import apply the same rule. Artifact bundles go from 16 to
64 MiB and the bridge buffer of Kronn-launched agents from 32 to 64 MiB, so
a Page at the guard still exports, imports and reaches agents. The MCP
bridge keeps its 8 MiB request limit, so an MCP agent writes a Page of up
to about 8 MiB in one call (#252).
- Above 4 MiB, the HTML editor and the revision diff show plain text instead
of highlighting every line on each keystroke (#252).
- A Page's revision history no longer carries every revision's HTML; the
revision you compare is loaded alone (#252).
- The backend container accepts any host user or group id from 1 to
4294967294 (it used to fall back to 1000 above nine digits).
- The backend container starts as root only to give its `kronn` user your
`KRONN_HOST_UID` and `KRONN_HOST_GID` and to hand its volumes to that user,
then runs the backend and every agent as that user, never as root. The image
is built once for everyone instead of once per user. `docker compose exec`
into the backend now needs `-u kronn` to get the same user as the agents.
- A bare `docker compose exec backend ...` now opens a root shell (root's home,
root-owned files): use `docker compose exec -u kronn backend ...` to act as
Kronn.
- `make start` on a checkout runs the published image of `VERSION`, not your
local edits: use `make start-build` to run local code.
- CI: the macOS portability leg runs on pull requests only when the tested
modules change, and on every push to main, with a 150-minute limit
(KT-1535, KT-1533). Every `backend/scripts/ci/test_*.py` must be wired in a
workflow, and the MCP catalogue census test now runs (KT-1180, #286).
### Fixed
- A cut Exec stdout no longer reaches an agent silently (KT-1570). Past its
limit, a workflow Exec step's stdout ends with
`[TRUNCATED: kept X KB of Y KB — full output: <path>]`, the step summary
carries a warning, and the whole stdout (scrubbed, up to 8 MiB) is kept in
the run's artifacts directory as `{{steps.<exec>.data.stdout_file}}`.
Collectors, Quick Exec runs and stderr keep the old suffix and write no file.
- Agents can read run artifacts again (KT-1585). The run artifacts directory
lived in the data directory, which the vault protection (KT-1495) hides from
every agent, so no step could open a screenshot or report it was given. It
now lives under `KRONN_RUN_ARTIFACTS_DIR` or the platform cache directory, in
a `kronn-run-artifacts/<key>` directory of Kronn's own; a base inside or
containing the data directory is refused. Docker uses a new
`kronn-run-artifacts` volume. Existing live run directories are moved at
startup, dead ones deleted. Any Agent step can still read any run's
artifacts; per-run confinement is KT-1587.
- A task worker can edit its project's own `.cargo/audit.toml` and
`.cargo/config.toml`, and a refused tool call is no longer silent (KT-1541).
Claude Code's built-in sensitive-directory check asks on any path under
`.cargo`, even in `acceptEdits`, and a `--print` worker cannot answer: the
Edit came back refused with no reason. The worker's settings now answer that
one ask, for the worktree's own `.cargo` only, through a script that refuses
any path going through a symlink or leaving the worktree (the Kronn data
directory stays denied). A refused tool call is written into the task
worker's transcript with its reason, the "worker ended without a delivery"
notice repeats the last refusals of the current attempt, and so does the
delivery summary of a worker that delivers anyway. See `docs/gotchas/claude-code-sensitive-directories.md`.
- A quota refusal no longer sticks to an execution after the reset, and no longer
sends it to another provider behind the user's back (KT-1564, regression of
KT-838 / KT-593). A refusal that announced its reset expires at that instant:
the watchdog hands the execution back to the same provider, tier, model and
profile and the escalation closes, unless its campaign is paused or a person
moved it first. Falling back to another provider is now its own explicit
opt-in (`quota_fallback` on the campaign, a toggle in the launch dialog;
`quota_fallback_agents` on `task_exec_launch`), keeps the execution's tier,
returns to the original worker after the reset even when the fallback
provider resets first, and shows in the review card and the delivery report.
**Migration 286 turns off the fallback that campaigns used to get implicitly
from `allowed_agents`**: re-enable it per campaign with `quota_fallback`.
- One request no longer starts a cascade of media generations, and Stop is
final (KT-1481). Every agent generation is tied to the human message at the
base of its wake chain: a result or failure notification now says it is a
notification, not a new request; after a failed, timed-out or cancelled job
nothing more is generated for that request (no retry, no other provider, no
dropped references — KT-1420); a request may start at most six jobs; and a
notification only leads to a new generation that builds on its result. Stop
disarms the callbacks of running generations in the database, so a late
result cannot wake the stopped agent, restart included. A new human message
is a new request with a fresh allowance.
- A task is no longer marked `done` when its delivery is integrated into a
campaign or feature branch that has not reached the default branch yet
(KT-1519). It stays `in_progress`, integrated and awaiting the merge; the
planning page, the board and the discussion plan say « Intégrée dans
<branche> — en attente de fusion ». The next task of the campaign still
launches on top of it, and a campaign whose tasks are all integrated
completes. The task closes when the default branch holds the integrated commit
(local or `origin/<default>`, which the sweep fetches first), when GitHub
reports the pull request that merged its branch, or its own `github-N` issue
closed (a squash merge never makes the commit reachable; an issue or pull
request the task merely links proves nothing), or when a human closes it;
Kronn checks at boot, after each integration into the default branch and every
five minutes (migration 283). The Planning list, the project Tasks panel and
the discussion monitor show the wait too: the plan counters have an
`awaiting_merge` bucket apart from `in_progress` and `done`.
- The dark-theme contrast scan no longer fails on `:root`, the name axe gives
a node it could not address once the page re-rendered it; the Settings route
left one such node behind on any dark theme, one run in two.
- The boot screen's fade-out tolerates a torn-down document, so a settle
timer that outlives its test no longer throws `document is not defined`.
- Every release tag now publishes the backend and frontend Docker images
(`ghcr.io/docroms/kronn-backend` and `kronn-frontend`, linux/amd64 and
linux/arm64, built on native runners). From the release that ships this
change (0.15.2), `make start` and `./kronn start` pull the image of the
checked-out version and start it: they no longer compile the backend on your
machine. A checkout of 0.15.1 or earlier keeps its old `make start`, which
compiles; republishing the 0.15.1 images does not change that, so a machine
under ~20 GB should use the desktop installer until 0.15.2. Before, a Docker install built the
715 000-line backend crate locally, which needs about 19 GB of memory, so a
machine with less (for example WSL with 5.8 GB) ended with `rustc` killed
after minutes of work (KT-1468).
- `make start-build` (or `KRONN_BUILD_FROM_SOURCE=1`) builds from source; an
image that cannot be pulled and is not on the machine yet (a `VERSION` with no
published image, e.g. right after `make bump`) falls back to it. A checkout of
`main` keeps the last release's `VERSION`, so `make start` runs that release's
published image, not the local code. An image that cannot be pulled but is already
local (an offline restart) is started as it is, without building.
`KRONN_IMAGE_TAG` picks another published tag; quotes around it in `.env` are
ignored and `${VAR}` references in it (`KRONN_IMAGE_TAG=${RELEASE_TAG}`) are
resolved as Compose does, instead of being passed on literally.
- Before any source build, Kronn reads the memory Docker can use (plus
swap). Below 20 GB, the need of the default build (19 GB peak), it stops at
once and names the alternatives: the published image, the desktop installer
for your platform, a bigger machine. The message carries the measured numbers
(19 GB peak; 10.4 GB and 12 minutes with one codegen thread).
`KRONN_FORCE_BUILD=1` builds anyway.
- The `latest` image tag follows the highest stable release only: republishing
an older release from the workflow no longer moves it backwards. It is moved
by its own job, one run at a time per image (a queue, nothing is cancelled
mid-write), which points `latest` at the highest published stable release at
that moment: runs that overlap in any order end with `latest` on the newest.
A registry or `git ls-remote` error in that job fails the run (after a short
retry), whichever release is the highest, instead of a green run that leaves
`latest` unchanged; only a registry that confirms the highest release is not
published yet ("not found", "manifest unknown") leaves `latest` alone.
- An empty `KRONN_IMAGE_TAG` in the environment wins over `.env` and means the
`VERSION` default, exactly like Compose; only an unset one reads the file.
- A project's task count was three different numbers: the Planning sidebar,
the project's Tasks tab and `task_list` each counted their own way. They now
share one definition: archived tasks are out unless asked for, a subtask is
a task, a multi-project task counts in each of its projects, and a task
belongs to a project through a direct link or a linked discussion. The
sidebar shows the backend's counts and lists every page, so a group lists
the rows its count announces; "General" counts exactly the tasks with no
project at all. The list stops after 5,000 tasks and says so. The
per-project and General counts come with the first page of `task_list` only
and are left out after a cursor, so an agent never reads an uncomputed 0 as
"no task without a project". A search in the sidebar lists exactly the tasks
the backend counted, including those matched only by their description
(KT-1351, #237).
- Opening an audit validation from the project page (audit timeline,
tech-debt validation or disputes link, docs check panel, a partial audit
that re-verified unchanged sections) opens that discussion again, instead
of the last discussion viewed, which could belong to another project
(#421).
- A foreach child refused for quota waits and resumes through its parent
instead of failing its item (KT-1556): 19 to 31 of 40 PR reviews were lost
this way. The parent run goes to `WaitingQuota`, reclaims the same child at
the refused step, and does not replay items that already failed in the same
sweep (no duplicate PR comments). Claude's short "limit · resets …" wordings
are classified as quota; "Context limit: reset the conversation" is not. The
run page no longer offers Resume on a sub-workflow child.
- A quota pause no longer spends a run's Timeout guard, which now measures
active runner time (persisted, so a restart keeps it). A reset announced past
the old deadline schedules a wake like any other instead of leaving the run
in `WaitingQuota` for good; runs parked that way by the old engine wake from
a reset less than a day old (KT-1562, KT-1563).
- A quota fallback chain remembers every provider that refused, so two
refusals without a reset escalate instead of bouncing (KT-1564).
- An undelivered worker parks as a pending resume and a woken worker adopts it
(KT-1540).
- A burst of peer wake-ups starts one room-agent turn instead of one turn per
message, which produced near-identical replies a minute apart (KT-1539).
- A room's native agent can follow, review, stop, resume and reassign the
execution it launched; before, only a joined CLI session could (#241).
- An MCP join moves the session link only after its binding is saved
(KT-1566).
- An HTTP agent past a tool's per-turn ceiling stops after a few refusals:
within one response, refusals past 8 are dropped and the model is told how
many; refusal-only rounds lead to a synthesis, then a hard stop; the room
gets at most two trace lines per tool and round plus a summary (KT-1545).
- Tool calls some OpenAI-compatible models write as `###Call:name{...}###`
text are stripped from HTTP chat replies, and a first turn made only of them
is retried once (KT-1543, #472).
- A 401 on a cached OAuth2 or token-exchange bearer clears the entry, mints
once more and retries once, instead of failing until the guessed hour runs
out (KT-1067, #282).
- The Copilot host sync keeps every hand-written MCP entry, like Codex, and
the user's Copilot or Codex config is copied to `.kronn-backup.1` before
Kronn's first write (KT-1036, #278).
- In Docker, network exposure follows `KRONN_BIND`: Settings shows a note
naming it instead of a switch that changed nothing and asked for a restart,
and the route refuses with the same pointer (KT-1066, #281).
- An unknown value or key in `config.toml` no longer resets every setting
(KT-1513). A credential this build cannot represent is never dropped: once
the encrypted store holds the credentials it is kept in
`config.toml.preserved.enc` (`0600`). See `docs/operations/key-management.md`.
- The "Add a project" bootstrap form lets the user pick the agent and
reasoning tier instead of silently running the first audit-capable agent;
the endpoint refuses with a 422 an agent the form would not offer
(KT-1591).
- A Kronn write in flight no longer sends re-read docs back to the
revalidation plan (KT-1590).
- Docs edited outside Kronn on a validated project are flagged on step 4,
which offers to approve them as they are (human only, refused while a
citation is broken) or to fix the broken citations first (KT-1489, #406).
- Audit card errors name the refused TD id and, when the central
`inconsistencies-tech-debt.md` exists but cannot be read, its real cause
(#258).
- Commits reachable from `origin/HEAD` are no longer checked as the worker's
identity trailers at delivery (KT-1498).
- An external API base keeps a host named `v1` while `/v1` normalization
stays pinned (KT-668).
- The CI verdict no longer goes red while it waits: earlier gate check runs
on the same head are marked "Superseded", a CI run that starts after the
verdict is accepted (KT-1230, #288), and a verdict that outlives its
upstream run stays pending and is re-armed when CI completes, instead of
cancelling itself red (KT-1586).
- Flaky tests fixed at the root: browser storage cleared after each frontend
test (KT-1057), a simulated restart that erased a parallel test's warm-up
claim (KT-1565), a disk-report check tripped by a temp dir name (KT-1567),
the anti-hallucination mode changed only through a locked, restoring
override (KT-1569), the OllamaCard registry re-check (KT-1588) and new-tab
detection in the routing e2e test (KT-1589).
### Security
- Settings › Security and system tells its three security mechanisms apart
(KT-1534). The authenticator has its own card (`#settings-authenticator`,
between the recovery phrase and the trust levels) with numbered enrolment
steps; it names the real `human-admin-secret` path from the server (macOS
`~/Library/Application Support/com.kronn.kronn/`, Docker `KRONN_DATA_DIR`)
instead of a Linux path given for every platform, says when agents can read
that file too, and how to get a new secret (`recover-admin-secret`). The
network access token card follows the server (`auth_token_set` on
`GET /api/config/network-exposure`) instead of this browser's stored copy:
after turning on network exposure it no longer says « désactivé » and offers
to create a token that would replace the one peers use. The risk panel puts
the missing authenticator first, links to its card and shows no
acknowledgement until one is set up; « coffre Kronn » becomes « dossier de
données de Kronn ». Once an authenticator is enrolled, the admin secret alone
no longer mints a publication grant, rotates the admin secret or rotates
another grant's secret: those routes ask for the strong confirmation too (a
holder renewing its own grant does not; before an enrolment nothing changes;
the `recover-admin-secret` recovery is untouched). The admin-secret hints
(first enrolment and replacement) say Kronn generated the secret (at first
start, or at the last recovery or rotation) and that it proves the machine
owner; once an authenticator is enrolled, enrolling or revoking a credential,
rotating a secret with it and replacing the authenticator also need its code
(listing does not). The claim about who reads the file is scoped to what the
sandbox covers: agents Kronn launches lose the data folder while its
protection is on, a CLI you run yourself can read it, so enrol before handing
it tasks.
- A safe plugin export no longer leaks a secret held in the plugin definition
(KT-1040). Without values the bundle replaced only environment values and
copied the definition whole: a token in a URL (query, path, user info, host),
a literal header, stdio arguments, a description, a TokenExchange body or
endpoint left in clear. The export is now an allow-list: names and env-key
names with no token-shaped word, `${ENV.…}` references, URL origins with a
clean host, plain endpoint paths and catalogue values stay; every other value,
the config label and env-key names included, becomes a named placeholder
`${ENV.KRONN_EXPORT_<FIELD>}` that the import review lists and asks for, and
that the import puts back with its JSON type (one left empty is named in a
warning and by the plugin's first call). Kronn's own plugin ids are kept when
the name they derive from is, and any other id gets a fresh one shared by the
configs of that server; a catalogue plugin round-trips with no placeholder.
The per-plugin export masks the same way, and its file import accepts the
placeholders and names those left; the export with values is unchanged. Hosts
and plain lowercase paths are only shape-checked, and so are name-like values
(names, labels, header names, scopes, the JSONPath, env-key and reference
keys, body keys, plain-slug ids): an all-letter word up to 20 characters and a
letter-digit word up to 8 (`OAuth2`, `GPT4`) outside a segmented run like
`4B7D-9F1C` are kept, so keep secrets out of them or use `${ENV.KEY}`.
- Agent processes Kronn launches lose the Kronn data directory to the OS
sandbox (Seatbelt, bubblewrap); the API port and the network stay open, and
`recovery.key` joins the secret names (KT-1495).
- Once an authenticator is enrolled, secret and destructive routes (vault
reset, import, token regeneration, recovery passphrase change, sensitive
deletions) require the strong confirmation, through one gate for every
caller. The recovery routes stay open (KT-1496).
- A bridge-token GET with a non-JSON body is refused with 415; an empty GET
is still accepted (KT-1068). A test keeps `Own` bridge routes on discussion
targets only (KT-1069, #283).
- `minimatch` 11.0.3 for GHSA-g3hv-75wg-m249 (ReDoS through eslint).
- The security scan is green again (KT-1533). `calamine` moves to 0.37, so `zip`
8.6 leaves both lockfiles and RUSTSEC-2026-0429 no longer applies to the
spreadsheet reader. The only remaining `zip` < 9 is the build-time copy
`utoipa-swagger-ui` 9 uses to unpack its own bundled Swagger UI. The advisory is
ignored for that path with a dated rationale in `backend/.cargo/audit.toml`, and
moving to `utoipa` 6 is tracked separately. `calamine` then moves to 0.38
for RUSTSEC-2026-0436: 0.37 panics or allocates without bound on a malformed
CFB/OLE2 file (a legacy `.xls`), and Kronn reads untrusted spreadsheets in
context files.
## [0.15.1] - 2026-10-22
### Added
- Custom skills keep a version history, like Quick Prompts. Every change
is saved as a full copy of the skill; a save that changes nothing adds
no version, and a skill written before this release gets its current
state as v1 at its next save. The skill page has a 🕒 history button:
the timeline of versions, a line diff with the previous version, and a
restore that asks first and records the restored content as a new
version, so nothing is lost. Agents can read the history (`skill_get`
lists the versions, `custom-x@N` reads one) but never restore a version.
Built-in skills have no history (KT-1434).
- Agents can create and edit custom skills, CLI agents (MCP bridge) and
HTTP/local agents alike, with the rules of Quick Prompts: `skill_create`
saves a skill in the room's or workflow's project, `skill_update` changes
a skill of a project in reach, never a builtin one. Each change is a new
version, so a step pinned to `custom-x@N` keeps what it was validated on,
while an enabled workflow loading the bare skill is disabled by an agent's
edit until a human enables it again. A human tries the new version on real
runs from the workflow's shadow settings. An agent never sets a skill's
`allowed_tools`; restoring a version and deleting a skill stay human.
- Agents know and can read what these versions and shadow runs need. CLI
agents (MCP bridge) and HTTP/local agents get the same tools and the same
guidance: `skill_get` lists a custom skill's versions and reads one with
`custom-x@N`, `qp_get` lists a Quick Prompt's versions and reads one with
`version`, `workflow_get` shows the shadow config. Both are read only and
scoped like the object itself: a project's skill or Quick Prompt, and its
versions, stay in that project. The step schema and the workflow guide's
new « Versions and shadow runs » section explain `external_effect` (mark
every step that acts outside Kronn), `{{run.variant}}` (keep the two runs'
branches and paths apart), `{{refs.<name>}}` / `KRONN_REF_<NAME>` and
pins; configuring or promoting a shadow stays human-only, and the agent
points the user to the workflow editor and the Page's Resources panel. HTTP
agents can now also author a workflow's `refs`.
- A workflow step, a Quick Prompt or a discussion can load a precise version
of a custom skill instead of the latest: `custom-x@3` loads version 3, and
later edits of the skill no longer change what that step runs. The skill
pickers of the workflow editor and of the Quick Prompt form show, for each
custom skill picked, « latest version » or a version to pin. A version
that does not exist is refused at save, never silently replaced by the
latest (KT-1447).
- An Agent step can also pin the version of its Quick Prompt (« Quick Prompt
version » under the Quick Prompt picker). Left unset, the step loads the
latest version, as before; pinned, an edit of the Quick Prompt reaches
the step only when someone picks the new version. A repository Quick
Prompt's pinned content is checked against its approval too, and an
imported workflow drops the pin, since its Quick Prompt comes without
history.
- Shadow runs: a workflow can start, alongside each normal run, a shadow run
on the same inputs that loads other versions of its skills and Quick
Prompts. Steps marked « acts outside Kronn » (posting, sending, writing
elsewhere) and Gates are skipped in the shadow run, so nothing happens
twice; it never counts against the concurrency limit, never pages on
failure, and its sub-runs are shadow runs too. It works in its own git
worktree, unless the workflow declares it never writes the checkout. Set
it from the « Shadow » button of the workflow page or in the workflow
editor (« Shadow runs »: on/off, the projects it covers, the version of each
skill and Quick Prompt). The run list marks shadow runs, and a normal run
shows its shadow side by side, step by step. Steps read the variant as
`{{run.variant}}` (`normal` or `shadow`). Agents cannot set it (KT-1449).
- A Page shows the resources behind it: the Quick Prompts and skills its
workflows use (those that publish to it, those its buttons launch, and
their sub-workflows), with their latest version, their history, a link to
edit them and, step by step, what the normal run and the shadow run load.
« Promote » hands a version tested in shadow to the normal runs of that
workflow: a skill in every step that loads it, a Quick Prompt pinned on
the steps that load it. The Quick Prompt itself, which other workflows
may use, is left as it is (KT-1450).
- Workflow and Page exports carry the custom skills their steps and Quick
Prompts use: each skill's file, name, version and a content hash. On
import, a skill identical to one already here is reused, a missing one is
created, and a skill with the same name but other content is shown in the
import preview, where you choose to create a copy (the default) or use the
local skill. Built-in skills stay plain references. A skill neither here
nor in the file is listed before the import and reported after it, never
dropped silently. Older export files still import (KT-1446).
- Workflows can name the objects they use: a Page, workflow, Quick Prompt,
Quick API, Quick Exec or skill gets a name in the editor's new "Named
references" section, and steps write `{{refs.<name>}}` instead of pasting
its id; Exec steps also get it as `KRONN_REF_<NAME>`. Saving checks the
object exists and belongs to the workflow's project or to none. Exports
carry the named objects and imports point each name at the imported copy;
a file missing one of them is refused with the name it lacks (KT-1448).
- Local image and video generation through ComfyUI (KT-1381). Config ›
Agents › Local gains a ComfyUI card next to Ollama: server state and
version, the one-command install on macOS (`brew install --cask comfy`), and
Image / Video pickers listing the installed models Kronn has a measured
workflow for (FLUX.1 schnell, SDXL such as RealVisXL, Wan 2.1). Once a model
is chosen, ComfyUI is offered wherever media is generated (new
conversation, the Assets Creator) and in `agent_list` as `local:comfyui`,
so agents generate on it through `media_generate`. Local generations cost
nothing and never enter estimates; ComfyUI never appears as a chat agent.
The card also downloads measured models (RealVisXL V5, FLUX.1 schnell, the
Wan 2.1 1.3B pack) into ComfyUI's own model folders, with progress, cancel,
resume and a disk-space check. Everything machine-dependent is a setting:
the server address, the install folder (default `~/ComfyUI-Installs`), the
workflow of a model Kronn cannot recognise on its own, and per-slot steps,
guidance, sampler and scheduler. A stopped ComfyUI, a job it lost or one
finished without a file fails at once with a sentence; a job that times out
or is cancelled is cancelled on ComfyUI by its id, never by a global
interrupt; downloads go only into the install folder of a ComfyUI on this
machine, never through a link, and are checked against their SHA-256
(macOS and Linux; Windows links to Hugging Face).
- A « hide orchestration messages » toggle beside « hide notes » in the
discussion composer (KT-1330). What the orchestrator posts into a room
(review requests, delivery summaries, terminal events, bounces, timeouts,
worker-undelivered alerts) now carries a typed `kind: "orchestration"`
(`messages.kind`; rows older than the column resolve it from their `orch-…` /
`delivery-summary:…` id). Hiding folds each run of consecutive ones behind one
expandable marker, persisted like « hide notes » (`kronn:showOrchestrationMessages`).
A question card, a campaign gate waiting on a human, a message with an open
action and a search hit are never folded. Display only: agents and the
bridge/MCP read paths still get every message. Orchestration messages no
longer raise the unread badge. See `docs/architecture/orchestration-message-kind.md`.
- Decision models are recognised (KT-1366). Kronn reads what Ollama's
`/api/show` says each local model can do, so a decision model such as
`clef-flash:9b` (which answers typed questions and cannot chat) is no longer
offered as a chat or agent model, and a launch on it is refused before it
reaches Ollama. The decision model is chosen like the tiers, with the same
picker: a « 🧭 Décision » row on the Ollama card and a Decision slot next to
image and video on an HTTP connection, each listing decision models only.
A local Ollama image model (such as `x/flux2-klein:4b`), or any model
whose declared capabilities do not include chat, is no longer offered as a
chat model either. An OpenRouter connection lists its decision models (Perplexity Decider,
Jev, Clef…), which OpenRouter returns only when asked for by output
modality. The manual catalogue form offers the `decision` capability, and a pull that
needs a newer Ollama says to update it instead of showing the raw error.
- The audit repairs more of what it used to fail on, and tells a human the rest
(KT-1357, 18/10 audit matrix). Kronn splits a list of lines of one file
(`deploy.yml:210,214,218`) into one marker per line, moves the comment that
trails a `path:line` out of the marker, and names an empty file in backticks
instead of citing it; the retry feedback for step 8 names each unaddressed
detector signal with the exact text to write (the flagged file's path, or its
name with the flagged line), and when the attempts are spent Kronn appends an
`## Unaddressed detector signals` section to the tech-debt index and sends a
`warning` instead of failing the run. The partial refresh of the TD index now
gives a malformed coverage matrix back to the agent, as the Full audit does
(a Node service's revalidation lost step 8 to its first attempt), and a refresh of a
section built from the whole source tree ends `unchanged`, with no agent, while
that tree is the one its baseline recorded; the glossary joins them (its
baseline now records the tree). An `unchanged` partial step is stored as done.
- The audit checks what its documents claim against the repository (KT-1387,
KT-1388, KT-1370, KT-1389; v1/v2 A/B KT-1358). A `make` target or a
`npm`/`yarn`/`pnpm`/`composer` script a document cites that no Makefile or
manifest defines is sent back to the step that wrote it; the commit
conventions are written from the observed history (`git log` pattern,
prefixes, language) and detected tooling, and called enforced only when a
GitHub Actions step under an established `bash`/`sh` shell or a dedicated
commit-message action runs the checker (a local hook, another CI system, a
Windows or matrix runner is « configured »); an unfilled commits template
fails its step. Stack facts read from lockfiles and runtime files are given
to the prompts, versions that contradict each other across documents are
flagged, and a debt is not declared resolved without proof in the code. A
citation marker no attempt could repair is demoted, after the last attempt,
to a flagged backticked path. All of these are advice measured on 754 real
audit documents (no false positive) and never fail a run: the partial
refresh feeds them back per attempt like the Full audit, they are recorded
as each step ends and survive a resume, and the audit timeline lists them
next to the unverified references. Repository files are read through the
project-rooted reader only, and a secret file is never cited by line.
- Testing a LiteLLM connection no longer calls a valid key rejected (KT-1412).
With no tier models, the test walks the catalogue; a model the proxy refuses
for this key (its allow-list or tag routing, answered 401/403 with a reason)
is a verdict on that model, so the next one is tried and the result says
"the key works, N listed models are not allowed for it". A refusal the
proxy does not attribute to a model still reports the key as rejected.
- The audit no longer reports committed secrets as absent, and its documents
agree on the tests and the CI (KT-1404; A/B KT-1358 round 3). The security,
Docker, tech-debt and final-review steps are given Kronn's own scan of the
tracked files: private keys (by name and PEM body), Slack / Teams / Discord
webhook URLs, well-known vendor tokens and literal values assigned to
token / secret / password names, listed by path and kind, never a value. A
listed path is a finding to revoke and purge whatever the file is named, and
« no secrets » is never written while one is listed. The project index, the
testing and operations documents and the final review are given the tracked
test files per runner, each CI workflow's events, each job's condition and
the runtime version each job runs; a trigger or a gate is described only as
the workflow states it (a label-gated CI never « runs on every PR », no
purpose the repository does not write, « blocks merge » is a repository
setting), and the final review makes every document agree with these facts.
The stack facts also carry the application's version and the direct
dependencies the lock file shows old (deprecated, abandoned, or an old major
of a short list), which the Dependencies and tech-debt steps cite; a debt
that rests on an absence (« no response caching ») must first look for the
mechanism in the code. These are prevention only: no new check was added.
- Arbitration cards are never lost (KT-1401). A `kronn-question` whose label,
question or context is too long is recorded with the text shortened at a word
(the full text kept in its description or context); a card still invalid is
shown as such with its reason and returned to its author in the
`disc_append` result (`invalid_cards`). Answering the last pending card of a
workflow step that has ended wakes the room's agent once with every answer;
an earlier card says its answer will reach that agent.
- A finished task execution gives its disk back (KT-1397). Its managed
checkout is removed once the execution is Done, Failed or Cancelled, nothing
uses it and it has been idle for `server.disk_sweep_idle_hours` (by finish
time and by the newest file written in it); the branch is kept and anything
uncommitted is saved first as two patches (staged, then working tree) under
the execution's evidence, so the exact state can be reapplied. A cancel with
« remove if clean » still removes only a clean checkout; a dirty or preserved
one waits for the sweep, which runs at start-up and every 30 minutes. Lane
build dirs (`target/wt-*`) whose checkout is gone are reclaimed when durable
lineage attributes them to a finished execution. A state over 256 MiB, with
unmerged paths or written recently is kept, with the reason recorded.
- A Definition of Done edited during an execution is adopted (KT-1402): the
execution re-reads it and the worker is told the new requirements, instead of
every delivery being refused. A refused delivery is recorded and shown in
`task_exec_status` (`last_delivery_refusal`) and in the escalation, and an
accepted one clears it.
- One guide for building automations, read the same way by every agent
(KT-1341). The built-in workflow architect is served section by section
through `tool_manual({tool: "workflow-architect", section})` to CLI agents
(MCP) and HTTP agents alike, and gains a « Reach » section: the workflow's
own project, the workflow scope a human sets (Projects accessible, every
project in red), why a step never inherits its room's grants, the parent
task over several repositories, and the general resources. HTTP agents get
`room_scope`. A refusal on another project now names the right way out for
its caller: `room_scope` for a room's agent, the workflow's scope for a step,
the principal for a worker (it used to name a `scope_request` tool that does
not exist). A test fails when a tool exists for one audience only, in either
direction, unless it is listed with its reason; the tools HTTP agents do not
have yet are deferred to KT-1356. The MCP catalogue does not grow.
- A full audit no longer ends Interrupted on what the audit could not fix
(KT-1353, audit matrix: a PHP site and a Node service). The documentary blockers the
committed documents held before the run are reported
(`documentary_preexisting`, `phase: final_gate`) and never charged to the
founding step, even when a step rewrites the document around them (a
re-verification appending to its history); the validation lets them through
only while the document is unchanged since the run. They are stored on the
run (`documentary_notes`), listed in the audit timeline and at the top of the
validation prompt, with the replacements Kronn applied; the blame ledger follows a
blocker through the rewording of its message, so a bare sheet citation written
by step 8 stays step 8's when step 10 grows the sheet. Where the gate's own
message carries the one replacement (a link relative to the document's folder,
the presence form of a secret file or of a tech-debt sheet, the single
did-you-mean path, a range one line past the end), Kronn applies it before the
step is judged and logs it (`documentary_autofix`); ambiguous suggestions are
never applied. A citation into a git-ignored path says so instead of
"does not exist", and the step 8 prompt shows how a sheet is cited.
- A workflow's project scope can open every project (KT-1336): read, or
planning write behind the same typed confirmation as a room, shown in red on
the workflow page and the launch card, pinned to the validated revision and
withdrawn on its own. The scope editor is redone: every project in one
searchable view with a None / Read / Write picker, the projects already
opened first, and one « Validate the scope » action. The editor is folded to
one line by default (KT-1355): the title, the granted access as chips (or the
red « Every project » badge, « No project access »), and a Details toggle that
opens the full editor. It opens by itself when a human has something to do:
a scope never validated, or suspended by an edit of the workflow.
- A merge no longer bounces on doc citation line anchors (KT-1321). The
integration merge, and the merge a worker finishes through `task_exec_commit`,
stop before the merge commit: a doc conflict that differs only by the line
numbers of `[src: file: …]` markers is resolved to the target's text, then
every citation to a changed file the merged tree left broken is re-anchored
by mapping its lines from the side it was written against to the merged file
(the `#symbol` must lie in the new range; the nearest occurrence to the
mapped position, within 20 lines and with no tie, settles a changed range;
otherwise the merge bounces on the doc). Only `docs/` and `backend/docs/`
are touched, from the merged index: a doc to rewrite, or a cited file, with
unstaged work makes the merge bounce instead of sweeping that work into the
commit. Both land in the signed merge commit. Any other
conflict still bounces. `make docs-reanchor` also runs where no
target-branch merge-base exists (a worker worktree) and, during an unfinished
merge, maps from both sides of the merge without a `BASE`.
- Kronn no longer fills the disk with per-lane build dirs (KT-1318). Every
parallel task lane builds in its own `target/wt-<lane>` (5-30 GB) and nothing
ever removed them: 195 of them took a 1.8 TB disk to 683 MB free. Now an
orphan sweeper (boot, then every 30 minutes) removes the build dirs Kronn
created, whose execution is finished, with no worktree, unwritten for
`server.disk_sweep_idle_hours` (default 6, minimum 1: `0` is read as 1, never
as "no idle check"); a dir Kronn did not create
(no ownership marker) and an active lane are never touched. Below
`server.disk_launch_min_free_gib` (default 30) a worker launch or a full
audit sweeps first, then is refused with the reason, the execution parked
`Blocked` and an alert in the principal room. **Settings → Disk**, the
`GET /api/maintenance/disk` report and the `disk_usage` MCP tool show free
space and Kronn's usage by kind and by task, with a Purge action that takes
orphans only (a named entry needs a confirmation). The `disk_usage` MCP tool
is read-only and scoped to the agent's project; the purge is a human action
(the bridge token is refused on it). See
`docs/operations/disk-usage.md`.
- « Revalider l'audit » now re-reads the docs whose sections are all
unanchored (no citation, no repository path), once per change (KT-1292).
Such a doc has no verdict, so nothing ever sent it to an agent. A review
stamp in `citations.json` (the doc's hash and the digest of its owning
step's sources) queues it for that step when it has none, or when the doc
or the step's sources changed since. After the pass the doc is stamped
whether or not the agent added an anchor: the plan is clean again and the
pass does not come back. The panel shows « re-read on <date> » for each.
- A principal can approve a delivery with a DoD item carried to another ticket
(KT-1291): `dod_verifications: [{dod_id, met: false, carried_to: "KT-1290",
evidence}]`. The approve integrates through the normal path; the item is
detached from the task and a `dod_carried` event names the carrier. The
carrier must exist, be open, share a project with the task and state the
requirement the worker was briefed with (the execution records its DoD
sentences at launch, and a `task_update` records the items it removes), or
the approve is refused with the cause; the detach runs in the review
checkpoint's transaction. A manifest that references a DoD id removed or
carried after the delivery is judged against the execution's launch-time DoD
snapshot instead of being refused as an unknown id.
- A `#symbol` citation anchor that names a definition the cited range
contains must open that range (KT-1074). When the range holds a definition
of the symbol (`fn`, `def`, `class`, `struct`, `enum`, `const`,
`interface`, `function`, `type`), that definition opens it: its doc comment
and attributes, then up to 5 sibling lines (a constant and its comment),
may sit above it. A range that holds no definition only has to name the
symbol, so a call site or a usage still backs a claim about wiring. A
range that starts a function early fails the docs check and the runtime
verifier with `a definition anchor must start the range`, and
`make docs-reanchor` proposes the range starting at the doc comment or
attributes of the definition. 213 citations of `docs/` were repaired by
reading their sentence (193 flagged by the rule, 15 moved by integrations, 5 more found by
the review sample);
the full check over every doc is green.
- Doc relevance in « Revalider l'audit » (KT-1168). Beyond citation
fingerprints, Kronn judges every section of the AI docs against the code as
it is now, deterministically and without a model: `anchors_intact`,
`outdated` (a cited line changed or vanished), `no_longer_relevant` (every
anchor is gone) or `unanchored` (no citation, no path: listed as
unverifiable, never called true). A backticked path counts as gone only
when it resolves nowhere (project root, the doc's folder, the tail of any
project file; not generated or git-ignored), and examples, history lines,
proposals, templates and changelog docs are not claims. The panel shows the
counts, the evidence and a proposed correction per section, the agent pass
receives them (unanchored sections as « check or anchor »), and the sources
badge turns to « to re-verify » with the vanished paths counted apart from
the broken citations. A vanished mention is accepted per section with
« Vérifié, inchangé » only, and from the project card only the paths its
stale block displayed; a validation or a revalidation carries that
acceptance over and never adds to it. The history and example rules apply to
the sentence holding the path, not to the whole line, and a proposal heading
must be a proposal phrase (« Ideas », « Future work »), not a word inside a
real title.
- A room can reach other projects, decided by a human (KT-1270). A room keeps
its own project: its working directory, where what it creates lands, and the
only project whose credentials and API configs it may use. In the room
settings a human opens another project in read (its tasks, rooms, runs,
pages) or in write (planning only: create and edit its tasks), or every
project, the latter in write only after a typed confirmation sent with the
scope the screen showed. An agent asks with the MCP tool `room_scope`; the
request grants nothing until a human approves it, narrows it to read or
declines it. The header always shows what the room reaches (a red badge for
every project) and the requests waiting. Grants are re-read on every agent
call, so a revoke applies at once; they never open another project's
credentials, never run its workflows, never pass to worker rooms, and are
kept as a journal with a `[kronn-scope: …]` line in the room for each change.
Only the room's own agents reach its grants, never a workflow step or a
worker posting in it. A workflow's runs reach other projects through a scope a
human sets on the workflow page; any change to the workflow suspends it until
validated again, and the launch card shows it. Design and refused attacks:
`docs/design/discussion-project-scope.md`.
- AI documentation template and audit v2 gap audit (KT-819). The project
facts of the briefing gain « Documentation externe » (Confluence, Notion,
wiki, other; https addresses only, no credentials, at most 6), rendered in
`docs/project.md`; a Confluence space is read back with one read-only GET
through the project's Atlassian connection, and a space the connection
cannot see is « not verified », never a mismatch. The audit seeds
`gotchas/known-traps.md` (the briefing's traps), `people/team.md` (roles,
names, contacts and the team answer) and `conventions/git-workflow.md`
(the answered Git facts) from the briefing, and lists the existing
documents moved to `legacy/` that read as conventions, gotchas or people in
each folder's README; a generated file carries the briefing marker, is
regenerated by each audit and is yours once you delete that line. The
project index's Stack table has `In place`, `Latest stable` and `Support
end` columns filled only from the dated version sources of the run
(`not verified: no source` otherwise), and a non-blocking `stack_currency`
diagnostic names the rows that lack their source and date. Kronn removes the
legacy « FIRST-RUN TASK » bootstrap block from a docs entry file at boot,
audited or not, without cutting what a person wrote around it.
- Tasks show which agents are on them (KT-154). A task carries one or more
assignees, each an exact identity — the room's agent, a punctual agent or a
joined CLI session — with an optional owner / reviewer role, so two Codex
CLIs stay distinguishable. The discussion plan shows them as pastilles on
each row (Focus and All views) and the worker of a running task execution
appears there too, with its agent and tier, without any manual assignment.
Assign and unassign from the plan row's quick menu or from the task sheet;
agents use the new `task_assign` MCP tool (`unassign: true` removes), and moving a
task to in_progress assigns the calling CLI as its owner when nobody is on
it.
- The briefing records the team's rules and bans (KT-1193). « Règles et
interdits » holds up to 20 one-line entries of at most 200 characters,
each a rule (always) or a ban (never). A ban may carry a pattern Kronn
checks: a dependency declared in `package.json`, `Cargo.toml` (dotted keys
included), `composer.json`, `requirements*.txt` or `pyproject.toml` (PEP 621
and Poetry), a module read from each language's import syntax (comments
skipped; `lodash` matches `lodash/fp`, not `lodash.debounce`), or a file
glob matched in linear time (at most three `*`/`**`). The entries go to a
`## Project rules` section of `docs/briefing.md` with a machine copy, put
back after the AI review, before `## Kronn options`; a briefing without
rules keeps its earlier bytes. Kronn writes them into a generated section
of the coding-rules doc (in the detected docs folder), restored after
every audit step, so every agent of the project reads them; a section that
lost its end marker is repaired without touching human lines. The briefing
cannot be saved while an audit runs on the project (409). The steps
that judge the code (tech debt and every `inconsistencies-*` step) get a
short rules block: a violated ban or unmet rule is a TD with its
`file:line` proof, and no fix may break a ban. Each ban with a pattern is
checked by a read-only detector (`project-ban`, no symlink followed, size
and count caps) whose hits join the step 8 signals and need a disposition.
- The briefing form reopens with the saved answers, and every project fact
takes an optional note (KT-1196). `GET /api/projects/{id}/briefing/answers`
reads `docs/briefing.md` (or the DB notes before the project has a docs
folder) back into the six questions, the project facts, the existing
documentation and the Kronn options; unknown or malformed parts are left
empty, and a section the form does not know stays inside the last answer
so a re-save keeps it. Saving again unchanged rewrites the same bytes.
Each fact (tracker, project key, forge, repository, default branch, commit
convention, sign-off, merge strategy, review, release cadence,
environments, people) gets « Note (facultatif) », up to 300 characters,
validated like the other briefing texts, stored in the machine copy (absent
when empty, so older briefings stay byte-identical), rendered next to its
fact in `## Project facts` and `docs/project.md`, given to the audit with
the fact and put back after an AI review.
An answer line that would read as structure (a heading, a machine copy, a
Kronn option line) is saved behind a `\` and given back as typed, and the
questions are read in order, so an answer can never split the briefing.
A click that only focuses a field no longer skips the prefill; when the
saved briefing arrives after a real change, the typed text is kept, only
the empty sections are filled from it, and « Charger le briefing
enregistré » loads the rest, so a save never wipes what was not changed.
The rules and bans (KT-1193) reload into their editor too, following the
same late-prefill rule, so re-saving a reopened briefing no longer drops
them; `## Project rules` ends the last answer instead of being read into
it, and the reopened form is refused (409) while an audit runs.
- The briefing asks about existing documentation and linked projects
(KT-999). « Existing documentation? » lists the locations Kronn finds in
the repository (`README*`, `docs/`, `doc/`, `wiki/`, ADR folders,
`CONTRIBUTING*`, root `*.md`, OpenAPI/Swagger files, `apidoc/`; symlinks
skipped), pre-checked, each with the date of its last commit (`git log -1`,
literal pathspec, bounded and timed out). A path can be added by hand: it
must be relative, without `..`, existing, and stay inside the repository
once symlinks are resolved, otherwise the server answers 400. Each kept
location gets a closed state (recent, old, obsolete, partial, reference), a
one-line note and, at most one per kind, the source-of-truth mark (never on
an obsolete location). The answers go to a `## Existing documentation`
section of `docs/briefing.md` with a machine copy, put back as answered
after the AI review, before `## Kronn options`. The section stays out of
the audit and review prompts until a bench run shows a gain. The linked
repos drawer is available in the briefing with one-click suggestions whose
description is left to fill in.
- Linked repos show which AI doc they really have (KT-999): « doc IA »
(`docs/AGENTS.md` or the legacy `ai/index.md`), « README seulement » or
« pas de doc IA ».
- The audit can read the briefing's existing documentation (KT-999), off by
default: `audit_briefing_docs = true` in `config.toml`, or
`KRONN_EXP_BRIEFING_DOCS=1` for one run (bench). It reaches only the steps
that use it — step 1 (`AGENTS.md`), step 8 (tech debt) and the API design
audit — as a `## Existing documentation` block within the 6,000-character
briefing cap (the block takes at most 3,000 of them, the briefing text gives
way). A `reference` location is read, reused and cited; an `obsolete` one IS
tech debt: step 8 records a TD row and a detail file citing the location and
its last-commit date whatever the audit's scope, and no step copies a fact
from it nor calls it out of scope — and on a later audit step 8 never parks
it under `## Reconciliation candidates` as « out of scope this audit »;
`old`/`partial` (and a state not provided) are verified before reuse, a
`recent` one is spot-checked. The recorded locations are re-validated before
they reach a prompt (a hand-edited `briefing.md` cannot add lines to it).
Disabled, the prompts (and the audit golden) are unchanged.
- Docs record what a human validated and ask for a reconfirmation when Kronn
changes it, never blocking (KT-1079). A validation (`validate-audit`, a TD
decision on a validation card, « Vérifié, inchangé », a revalidation that
leaves the docs as validated) records the digest of each validated doc in
`docs/provenance.json`, next to `citations.json`; the validated text stays
local under `.kronn/audit/validated-originals/` for the diff. No marker is
added to the prose. Every automatic writer now applies its change to a
human-validated doc or an `owner="human"` section instead of refusing,
reverting or skipping it: full and partial audits (no more restore + dated
`human-section-diff` report), revalidation (re-anchoring and the agent pass
now cover human sections), template reinstall (human sections are no longer
put back), documentary repair (citation bundles in human sections are
normalized) and the tech-debt index resync. The doc is then marked « to
reconfirm » with the writer and a before/after diff. A « À reconfirmer (N) »
button next to the audit status on the project card opens the docs to
reconfirm with their diff; « Confirmer » (one doc or all,
`POST /api/projects/:id/doc-provenance/confirm`, human only) restores the
provenance. The template reinstall plan names the human-validated content
it will rewrite before writing. The learning gate weights its sources by
this provenance: a source in a doc a human validated and nothing changed
counts as proof; one only an AI wrote, or changed since its validation,
does not count as verified evidence for a `fact` until reconfirmed. The
audit prompts let an audit correct a human section where the code
contradicts it. No database change.
- Settle the audit's tech debt from the project card (KT-982). A "Dette
technique" block shows the progress (confirmed, kept on purpose, deferred,
rejected, remaining), the remaining count per severity and the Validated
rule, and opens a review screen built for a hundred items and more: filters
(to settle, deferred included; settled; all), severity chips, a search over
id, title and cited files, a list grouped by severity with a checkbox on
every row, and a bulk bar that settles the whole selection in one atomic
request (one unknown id writes nothing). The detail shows the problem, the
clickable `file:line` pointers (opened in the code viewer), the verification
criterion and four decisions; keeping an item on purpose records the reason
in the project's decision log, trusted as a human decision like a validation
card. Each item links its Kronn task or creates one (the KT-1167 conversion
for one sheet); GitHub and Jira are disabled placeholders. "Discuter de cette
TD" shows the validation messages that name the item and the item's own
discussion, created with the agents picked in the comparative Quick Prompt
selector (now a shared component). New endpoints:
`GET /api/projects/{id}/audit/tds`, `POST …/audit/tds/decide`,
`POST …/audit/tds/{td_id}/task`, `GET|POST …/audit/tds/{td_id}/talk`.
- A project can be validated without the validation discussion's terminal
marker once every Critical and High tech-debt sheet is confirmed, rejected
or kept on purpose (a deferral does not count) and the other gates pass
(KT-982). The discussion path still works.
- CI checks the `[src: file:]` citations of Kronn's own docs (KT-1059). A
test (`cargo test docs_citations`, run by the backend suite) walks
`docs/**/*.md` (except the historical `docs/releases/`) and `backend/docs/`
with the citation verifier's own extraction, path jail and line counting,
and fails on a citation that has no line, names a missing file or a
directory, is absolute or leaves the repo root, or points past the end of
the file. Only a binary file may be cited without a line. Fenced code and
placeholder examples (`<path>:<line>`) are skipped; a citation in backticks
is checked. The report lists `doc:line`, the citation and the reason. The
corpus was repaired to zero failures: every line-less citation now points
at the symbol it means, paths of moved or split files were re-located, and
sentences about code that no longer exists were rewritten.
- The audit's tech-debt baseline runs as code (KT-152). Fourteen new
deterministic detectors cover what the step 8 prompt used to ask the agent
to grep: Dockerfile (root user, no `HEALTHCHECK`, apt flags and list
cleanup, base image untagged or `latest`, `ADD` of a remote URL,
secret-named `ARG`), compose images untagged or `latest`, CI
(`StrictHostKeyChecking=no`, `pull_request_target`, actions on a moving
ref), tracked `.env` files (filenames only), templates (`<img>` without
`alt`, inline `<script>`/`<style>` over 50 lines) and OSS community files,
gated on OSS intent. Each finding carries `file:line` evidence, and step 8
is re-run when one is left unaddressed, like the existing detectors. PHP,
Node.js, Rust, Python and Go items (`display_errors`, `opcache`,
lockfile installs, `cargo deny`, `govulncheck`…) reach the prompt only
when that stack is detected. The step 8 prompt shrinks from 19,941 to
about 10,600 bytes (about 5,400 to 2,900 tokens); every former baseline
item is mapped to a detector, a stack item or a remaining prompt line by a
test.
- Cross-agent audit, experimental and off by default (KT-1183). A briefing
option (« Audit croisé ») picks 2 or 3 agents and shows the estimated cost
first: ×N to ×2N the security step's last measured single-agent cost (from
the same step figures as `GET /api/audit-runs/{id}/cost`), or the measured
cost of an earlier cross-agent run with as many agents. On the security
step only, the agents detect in parallel without seeing each other's work
and return their findings as a machine-readable block; Kronn checks every
citation (file, lines, quoted text) and drops a false one with no model
call, groups findings about the same place (information only), then has
each finding challenged by another agent, who is not told who found it.
Never a vote: a refutation dismisses a finding only with a proof that passes
the same citation check, otherwise the finding stays, marked contested; the
author may answer a proven refutation once, and two proven positions become
a human `kronn-question` card with both arguments side by side. At most 30
findings are challenged per step, the rest listed as not cross-checked.
Kronn writes the index and the TD sheets (an existing sheet keeps its body,
only a Kronn provenance block changes); every finding's provenance —
found by, detection count, checked by, proof, reply, verdict — and the
dropped and dismissed findings are stored (migration 257). Without the
option the step is unchanged, byte for byte.
- Citations can name what they point at (KT-1074):
`[src: file: path:120-140#symbol]`. The symbol must appear as a whole word
inside the cited lines; the check is textual, no code is parsed. The docs CI
check fails on an anchor that left its range and, when the symbol still
exists in the file, suggests the new range (`try path:436-465#symbol`). The
runtime verifier applies the same rule to agent and audit citations: a
missed anchor reads as `out_of_bounds` with a detail saying where the
symbol appears, so the existing red/amber badges are unchanged. Re-anchoring
a moved citation keeps its anchor. Documented in the citation spec and in
`docs/AGENTS.md`. Every one of the 1,345 file citations of
`docs/architecture/` and `docs/operations/` was re-read against the code
and now carries an anchor: about 60 % pointed beside what they described
(a random sample of 24 measured 10 beside before, 0 after); they were
re-pointed, and some 50 sentences describing code that has since changed
were rewritten (ADR-002 no longer claims an advisory integration lease,
the HTTP workspace catalogue lists its 13 tools including `git_commit`,
audits accept every agent but Vibe, among others).
- The docs citation check is scoped per pull request (KT-1195). A pull
request that changes no file under `docs/` or `backend/docs/` gets the
check as a counted warning (job summary and one annotation per failure),
never a failure; one that changes docs fails only on citations of the docs
it changes, with the repair command in the message
(`make docs-reanchor DOCS="…"`). A pull request that changes `VERSION` (a
release), a release run without a pull-request proof, a manual run and a
local `cargo test` stay strict on every doc. CI lists the changed paths
from the pull request's merge commit (`scripts/ci/docs_citations_changed.sh`,
failing rather than guessing when it cannot) and passes them to the test.
`make docs-reanchor` re-points a citation whose `#symbol` left its range
only when the symbol occurs once in the cited file, keeping the span length
(clamped) and the anchor; with several occurrences it rewrites nothing and
lists the candidate lines, and the CI message no longer suggests a « try »
range for such an anchor (the nearest occurrence was often the wrong one). Missing files, missing lines and lines past the
end are never rewritten, and only the checked docs are written. It is the
first step of the release checklist (`docs/operations/releasing.md`). The
CI check, the `verified` badge and the runtime verifier keep one anchor
rule with no tolerance window, now stated in the citation spec. The 16
citations the integration had shifted were repaired: 1 by the command, 15
by hand, because their symbol occurs several times (for one of them the
nearest occurrence was the wrong one).
- The AI doc folder is configurable (KT-263). Settings › Project scanning sets
a default folder relative to every project root, and each project can
override it from its page; without an override, detection stays `docs/` →
`doc/` → `ai/`. Detection, the audit badge, the evidence file, the context audit, the
redirectors and the doc viewer all read the same resolver. A folder that is
absolute, uses `..`, points into `.git` or reaches outside the project
through a symlink is refused with a 400 when saved and fails closed when
used. New column `projects.docs_dir` (migration 258).
- The audit pipeline writes under the project's AI doc folder (KT-1192). One
root, resolved per run: step targets, TD sheets, `decisions.md`,
`briefing.md`, `project.md`, the `inconsistencies-*.md` indexes, checksums,
the installed templates, the root redirectors and every path the step,
validation and briefing prompts quote follow it, for Full and partial
audits, sub-audits, revalidation, template reinstall and merge. A project
without an override keeps `docs/` with byte-identical prompts. The legacy
`ai/` → `docs/` migration still applies only to unconfigured projects.
- The briefing asks for the « Dossier de la documentation IA » (KT-1192),
pre-filled with the project's override, else the folder it already
documents itself in, else the default from Settings, else `docs/`. Saving
it sets the project's folder through the same containment check (400 on
refusal); the default from Settings now only pre-fills this field and
never applies to a project by itself.
- No audit starts without a briefing (KT-1192): Full, partial, sub-audit and
revalidation launches answer `error_code: briefing_required`. In the UI
the briefing step is required and its form gets a one-click « Utiliser
les valeurs par défaut ». An MCP launch (`audit_launch`, `audit_prepare`)
receives the briefing as ready-to-post `kronn-question` cards in the
discussion's language, as a set of its own (`briefing-<field>.<set>`
keys); once every card of the latest set is answered in that discussion,
Kronn saves the briefing through the form's validation, and answers to a
superseded set are saved nowhere. A launch with no
discussion (a workflow run) gets default answers, flagged « valeurs par
défaut — à compléter » on the project card.
- An existing AI doc folder is handled explicitly (KT-1192). Kronn's own docs
are updated in place without a question; they need both a `.kronn.json`
recording an audit or a bootstrap and a structural marker (a line-leading
`kronn:doc-version` / `kronn:generated` header, paired `kronn:section` or
managed-block markers outside code, or the AGENTS.md signature). A mention
of Kronn, or markers without that record, is a human's docs.
A folder of human docs shows « Le dossier … utilisé par Kronn existe déjà »
and asks: put Kronn's docs in another folder, or move them to
`<folder>/legacy/` (file count, broken links and a detected mkdocs,
Docusaurus or VitePress config are stated first); the audit then reads
them as its primary source. The silent pre-audit move is gone: nothing
moves without that choice, never into an existing `legacy/`, and only once
the briefing is committed (a failed save leaves the folder as it was). An
audit or unattended launch on such a folder without a choice stops with
`docs_folder_choice_required`.
- Audit versions and standards are judged from dated sources (KT-1000). The
tech-debt step and the security pass get a « Technologies and standards »
block: the project's stored dependency watch (version in place, latest,
gap, check date; never refreshed by the audit) and the runtime versions the
repository pins (`node-version`/`python-version`/… in `.github/workflows`
and `.gitlab-ci.yml`, `.nvmrc`, `.node-version`, `.python-version`,
`.ruby-version`, `rust-toolchain(.toml)`, `.tool-versions`), each with its
citation. Kronn picks the live source from configuration — context7 when the
agent mounts it from the project's MCP config, else a web tool when the
agent has one (a direct Claude CLI with full access), else the dependency
watch — and the prompt states it. New citation kind
`[src: deps: <manager>/<package>@<YYYY-MM-DD>]`: in an audit it is checked
against the injected watch (absent row or other date = fabricated);
elsewhere it stays unchecked. The `Dependencies` coverage row must state
`source used: …` or `not verified: no source`, otherwise step 8 is re-run.
- « En faire des tâches Kronn » on the card of a project with tech debt
(KT-1167). `POST /api/projects/:id/audit/td-tasks` turns every
`docs/tech-debt/TD-*.md` sheet into a planning task linked to the project:
one parent task per theme (the sheet's `Category`, else its `Area`) with one
subtask per sheet. A subtask carries the sheet's title, its severity as
priority (Critical/High/Low as is, Medium and unknown as normal), theme,
area, status, problem excerpt, sheet path and a verification criterion as
its Definition of Done. Every task is keyed (`audit:<project>:<TD-id>`, and
`audit:<project>:theme:<theme>` for a parent) on the existing planning
idempotency key, so a second click creates nothing and reports the tasks
already linked; rejected sheets and sheets covered by an accepted decision
are skipped. The answer counts created, already linked and skipped. The
sheets stay in the repository as the source of truth. The briefing form
gains « Faire des TD trouvées des tâches Kronn »: saved in `docs/briefing.md`
(and the DB notes) as a `## Kronn options` line, it runs the same
conversion at the end of a successful Full audit (`td_tasks` SSE event, a
warning on failure).
- The audit recap now shows what a run consumed per phase and in total
(KT-1084): the briefing, the audit steps and the validation discussion, each
with its tokens (uncached input, output, cache) and cost, plus a grand total.
`GET /api/audit-runs/:run_id/cost` adds them up from what each phase already
stored, nothing is re-measured. A step a resume carried over is counted once,
at the run that really ran it. A phase no agent reported shows « non mesuré »,
never 0; a sum over an unknown cost is marked `≥`, an estimated one `≈`. The
briefing is linked to the project, not to a run: it is the briefing discussion
opened since the previous Full audit.
- « Revalider l'audit » on the card of an audited or validated project
(KT-1168). Kronn first checks every citation against the fingerprints
recorded at validation, without a model:
`GET /api/projects/:id/revalidation` lists, per doc, the moved citations,
the broken ones (changed, removed, file missing), the ones inside
`owner="human"` sections, and the audit step that owns each doc.
`POST /api/projects/:id/revalidate-audit` then rewrites the line number of
each moved citation whose content is unchanged, also without a model. If
any doc still has broken citations, it runs the step that owns that doc,
and only that step, through the partial-audit pipeline (same lease, run
row, Stop and gates). That step gets the re-verification prompt, Kronn's
findings for its doc and the new builtin skill `audit-revalidation`. A
citation that no longer resolves is named before the agent runs, and the
step cannot end `unchanged` while it is still there. Human sections are
listed for their owner and never edited. Docs the agent rewrote get fresh
fingerprints, so the « Sources » badge is clean again once nothing is left.
A run that rewrote nothing, because the agent ended `unchanged` or was
stopped first, restores the validation it revoked. Relaunching the action
resumes: it recomputes what is left. Each section of the panel has
« Vérifié, inchangé », wired to the existing acknowledgement, and every
error, stop or interruption offers « Reprendre ». The routes are not on the
bridge list.
- The briefing now asks about the project itself, on closed cards, and the
audit writes `docs/project.md` from the answers (KT-1173). Next to the six
questions, the form shows one choice card per fact: tracker, project key,
forge, repository, default branch, commit convention, sign-off, merge
strategy, review, release cadence, environments (with optional URLs) and
roles (arbiter, tech lead, DevOps, PO, QA, with only the name and contact
the user types). The backend refuses any option outside a card. Answers go
to a `## Project facts` section of `docs/briefing.md`, kept as answered
even when the AI review rewrites the file. Each Full audit renders
`docs/project.md` deterministically (template v2 ships its skeleton and
routes it from `docs/AGENTS.md`). An open repository is taken from the git
remote. When the project has a Jira (`mcp-atlassian`) or GitHub
(`mcp-github`) API connection, the Jira project key, the repository and
its default branch are checked read-only with one GET each, and a
mismatch is flagged on the page. With no connection, the value is marked
"not verified". A `docs/project.md` without its generated marker is never
rewritten.
- « Recréer les fichiers du template » (KT-1166): a damaged or missing AI-doc
template file can be reinstalled on purpose. The project card lists the files
that would be replaced and asks for confirmation; every replaced file is first
copied to `.kronn/audit/template-backups/<timestamp>/`, then the result names
what was replaced and where the originals are. Only the template's own files
are written and `owner="human"` sections are kept as they are. Files still
template-like or broken (empty, unfilled `{{placeholders}}`, unpaired section
markers) are repaired by default; files holding real audit content are listed
apart with a warning, unchecked, and replaced only when ticked (the POST takes
the explicit path list and refuses any path the plan does not offer). The action is
refused while an audit holds the project's lease. It is a human action: the
two routes (`GET .../template-reinstall/plan`, `POST .../template-reinstall`)
are off the bridge-token list and refuse a bridge caller. A failure keeps
« Reprendre ».
- After an audit with failed steps, a single step can be retried alone, or a
human can accept it as it stands (KT-1131). On each failed line of the
timeline, « Relancer cette étape » resumes the interrupted run with
`retry_steps` (the other failed steps stay failed and unrun), and
« Accepter en l'état » calls
`POST /api/projects/:id/audit-runs/:run_id/steps/:step/accept`, which
records who, when and an optional reason (migration 246). The route is off
the bridge list and refuses any agent token. It is refused when the target
is missing, empty or still a template. Once no failed step is left,
« Finaliser l'audit » completes the run without an agent step, so the
project can be validated without a global audit. The recap and the
validation discussion list accepted steps apart from succeeded and
unchanged ones.
- A repository that already has its own docs can now receive the AI-doc
template v2 by merge (KT-1172). `GET /api/projects/:id/template-merge/plan`
shows, without writing anything, a correspondence table that maps each
existing doc, section by section, to a v2 file, plus one diff per file.
`POST /api/projects/:id/template-merge` (`{"base": "local"}` or
`"remote"` for the fetched default branch) applies it in a
`kronn/template-v2-merge` worktree: mapped sections are copied verbatim into
the v2 files that do not exist yet, existing files are never rewritten,
moved or quarantined, and the merge stops if any existing doc changes.
Unmapped sections are listed and stay where they are. Calling it again
resumes the same worktree. `.../accept` commits the files you keep on that
branch, and `DELETE` drops the worktree.
- The project's Audit tab now offers this merge (KT-1181). On a documented
repository that was never merged, "Lancer l'audit" first opens the merge
panel, which also lets you run the audit without merging. The panel shows
the correspondence table with a badge per action, with unmapped sections
highlighted as left in place, and warns about docs that git does not track
yet so you commit them first. You pick the local or remote base, then get
one diff per file that you accept one by one or all at once. You can also
resume or abandon the merge. Every error says what to fix and offers
"Reprendre".
- The template merge plan now says where a merge stands (KT-1182). Its new
`merge` field reports whether the `kronn/template-v2-merge` worktree and
branch exist, how many files wait for review, and how many commits the
branch has that the checkout's branch (and the remote default branch)
does not. It reads git only: no fetch, no write. The panel opens on "Fusion
en cours" with Reprendre and Abandonner when a worktree exists, and on
"Fusion acceptée, à intégrer (N commits)" when accepted commits wait to be
merged. "Lancer l'audit" no longer offers the merge again in that case.
- Audit drift is now measured per citation (KT-1175, KT-1078). Validation,
and every audit baseline, records a fingerprint of each `[src: path:line]`
the docs cite in `docs/citations.json`: path, line and a SHA-256 of the
cited line (whitespace-normalised, secrets redacted), never its content.
A section with fingerprints goes stale only when one of its cited lines
changes or disappears, and the drift report names the citation; a commit
elsewhere no longer marks it stale. A line that only moved, by up to 50
lines, is re-anchored. `GET /api/projects/:id/citation-drift` lists broken
and moved citations per section, and
`POST /api/projects/:id/citation-drift/acknowledge` (`{"doc": "docs/…"}`)
records a section as checked and unchanged without rewriting it. A new
"Sources clean" badge, kept apart from the dated "Validated" one, turns to
"To re-verify" as soon as a citation breaks, with no new audit. An
optional cron, `audit_reverify_cron` in `config.toml` (off by default),
re-verifies every project on that schedule.
- A decision is no longer reported as tech debt (KT-1174). The v2 template's
`docs/decisions.md` is now a list of ADRs, with no cap on their number:
`## ADR-NNNN: title`, then status (`proposed`, `accepted`, `superseded by
ADR-NNNN`), date, optional `Supersedes`, `Covers`, `Source` and `Links`,
and Context, Decision and Consequences sections. After the audit steps,
Kronn settles every TD sheet an accepted ADR covers, either because the ADR
lists its slug in `Covers` (a re-discovery on another day still matches)
or because the sheet names the ADR in `Decision record`: its status becomes
`Accepted decision`, the sheet points at the ADR, and the TD leaves the
index and the validation. `proposed` and superseded ADRs cover nothing.
Only an ADR already accepted when the run started (in that version: a
`Covers` entry added during the run does not count), or accepted by a human
in a validation card, may settle a TD. An ADR the run itself wrote or
flipped to `accepted` goes back to `proposed`, and the finding stays in the
validation, where accepting it as a decision records a covering ADR. A
sheet's `Decision record` line counts only if that line, naming that ADR,
was on the sheet when the run started: one the run writes on a new sheet,
adds to an existing one or changes leaves the finding in the validation.
An ADR a human accepts during a run, or while it is paused before a
resume, counts from the next fresh run. A resumed run keeps the baseline
of the run it continues (migration 252, `audit_runs.adr_baseline` and
`audit_runs.adr_record_baseline`). The steps that write TDs receive only the ADRs
in force before the run. Existing tables keep working: a row that links a
TD sheet still covers it.
- Each agent reply says whether it continued the agent's conversation
(KT-1202). Every turn relaunches the CLI or ACP process; memory survives
through `--resume` (Claude Code) or ACP `session/resume`, with the full
history sent when that is not possible. The reply now records which
happened: « reprise », « nouvelle session » or « historique complet »
(resume refused by the runtime), shown discreetly after the model, with the
reason in the tooltip (no stored conversation, working directory changed,
previous turn unfinished, conversation expired, history changed, nothing
new, resume not supported…). Agents that never resume record nothing.
Migration 259 adds `messages.session_continuity`.
### Changed
- The dark theme is now VS Code's Dark Modern palette: flat greys,
`#0078d4` filled controls and `#4daafc` accents, no glow, scanlines or
aurora. It applies to everyone on Dark or on System with a dark OS, with
no setting to change. It is held to the dark-theme contrast floors (4.5:1
for every text role) and covered by the dark-theme axe pass. The former
purple palette, with its neon effects, becomes the secret Arcade theme,
unlocked with a code in Settings like the other secret themes.
- CI no longer reruns the whole suite on a tree a pull request already
tested green (KT-1170, reshaped by main's shared-cache change): a push to
`main` only builds and saves the caches pull requests read, and a release
tag reruns no test workflow.
- The macOS and Windows portability CI job compiles only the four suites it
runs. A new default Cargo feature, `full-tests`, gates every other
unit-test module; the job builds with `--no-default-features`. The full
lib-test binary (9 200 tests) made rustc peak above 13 GB while the
standard macOS runner has 7 GB, so the first step swapped for 45 to 55
minutes. The job also prints its rustc and machine facts, since two runner
images with different Rust releases rotate and invalidate the shared
cache. Every other job keeps the default feature set. The `with_conn`
unwrap lint exempts exactly the item carrying a `#[cfg(test)]` or
`#[cfg(all(test, …))]` attribute instead of cutting each file at its
first `#[cfg(test)]`, so production code after a `mod tests;` line is
scanned again. (KT-15)
- Every view has its own address, and the browser's Back, Forward and reload
follow it: the pages (`/projects`, `/discussions`, `/planning`,
`/workflows`, `/pages`, `/plugins`, `/config`), what is open in them
(`/projects/<id>`, `/discussions/<id>?message=<id>`, `/planning/<id>`,
`/workflows/<id>/runs/<id>`, `/workflows/qp/<id>` and the other Automation
tabs, `/pages/<id>`, `/plugins/<id>`) and the whole-window views
(`/standalone/pages/<id>`, with its view parameters such as `?tv=1`, the
Page and discussion mosaics). An address can be copied, sent and reopened.
The links written before — `#discussion-<id>`, `#project-<id>`,
`#page/<id>?…`, the mosaics' `#…/mosaic?…`, `#settings/artifacts?…` and
`#config` —
keep working for good, from Live Pages, messages, bookmarks and the `kronn`
CLI alike. The desktop app's embedded server answers a reload on any of these
addresses with the app.
- Ctrl/Cmd-click, Shift-click or a middle click opens a page in a new tab:
the navigation tabs, an Artifact's related discussion, workflows and
publication runs, and the rows and links that open a discussion, a
project and its views, a workflow and its runs, a Quick Prompt/API/Exec, a
task, a plugin or an Artifact, the "Fix" of the pre-launch agent check and
a skill's "Open in Config". A plain click still opens it in place; the
look of every element is unchanged.
- Views inside a page have their own address too: a project's view
(`/projects/<id>/code?file=<path>&line=<n>`, `/projects/<id>/git`, …), the
workflow wizard (`/workflows/new`, `/workflows/<id>/edit`), a comparison of
discussions (`/discussions/compare/<run>`) and a Configuration section
(`/config#<section>`). A file an agent cites, a just-validated audit or a
just-improved Quick Prompt now opens at such an address, so Back, a reload
and a shared link land on the same view. A code view opened without a file
names the file it shows in its address, so Back to it reopens that file;
choosing a type's list from one of its Automation resources is a step Back
undoes; a reload, Back or Forward on a Configuration section brings it
into view again.
- "Open the retention setting" on the run-retention banner (Automation and
Configuration) lands on the setting, centred and focused, at its address
`/config#run-payload-retention`; it used to stop short of it while the page
was still loading. Every Configuration anchor (`/config#<section>`) waits
for the page to settle the same way, and the sections in Configuration's
side menu are links: Ctrl/Cmd-click opens one in a new tab.
- A release tag only checks the version, builds and verifies the installers
and creates the draft; it no longer reruns the CI suites the merged pull
request already passed.
### Fixed
- A skill imported with a workflow or a Page never pre-approves tools: its
`allowed-tools` is dropped on import, whoever imports it, and an import
whose skill still declares tools after that is refused. A human sets it
again in the skill editor if wanted.
- A worker's HTTP agent can no longer run a tool its catalogue leaves out
(backlog tools, `qp_update`, `skill_update`…) by naming it: the call is
refused before any access check.
- Deleting a workflow is no longer blocked by interrupted runs whose
worktree was removed since: only a worktree still on disk, which could be
resumed or discarded, holds the deletion.
- Integration validations that need Docker work again, on a human's
decision (KT-1433). Since 0.15.1 they run in the lane sandbox, which keeps
Docker closed, so `make iso-deps` and the like always failed and the
delivery was sent back to the worker as if its code were wrong. A
validation that fails because of its environment now holds the execution
with the fix instead, and when the sandbox closed Docker, Kronn asks on a
card: refuse, allow for this discussion, or allow for the project; an
answer that allows resumes the integration on its own. Project settings
gain « Accès des validations d'intégration »: sandbox only, Docker allowed
(the Docker socket alone is opened), or full access behind a warning.
- DOMPurify 3.4.17 (GHSA-cg2g-xh64-v4q7, critical: a split template bypassed
`SAFE_FOR_TEMPLATES`; GHSA-mrv2-3pqr-j5jr, high: an `IN_PLACE` hook removing
a parent).
- A release body stays under GitHub's 125,000-character limit: a CHANGELOG
section too long for it is cut between two entries and links the full
notes.
- An HTTP agent (Ollama, OpenRouter, LiteLLM…) can now use an image attached to
the discussion as the source of an image generation (KT-1418). It was told
only the filename while `media_generate` needs the context file id, so every
call was refused and the refusal, reported as a success, was retried until the
tool budget ran out. The image notice now gives each id, `media_generate`
also accepts an exact filename of the same discussion, and the refusal lists
the usable images. Agents are also told that a video model starts from a
picture when its `frame_positions` lists `first_frame` or `last_frame`, and
that an unknown reference count is not a refusal.
- A project granted to a discussion is now readable by its native Claude Code
agent (KT-1406). From the next turn the project's checkout is a read-only
directory of the agent (`--add-dir`; a write grant stays read-only on disk,
revoking, the last grant included, removes it at the next turn). The agent's
context, for Claude Code and Codex, names the granted projects with their path
and access, plus the projects it can ask for, with `room_scope` as the way to
ask the human. A refused read is explained by that context, not by `/add-dir`.
An agent in full access is unchanged. A checkout reached through a symbolic
link is never opened and a checkout swapped between the plan and the launch is
refused. Codex keeps its default sandbox (reads the disk as it is, writes only
in its working directory): no read isolation is claimed for it, that is
KT-1409.
- TLS certificate checks use rustls-webpki 0.103.16 and webpki-roots /
webpki-root-certs 1.0.10 (RUSTSEC-2026-0381, -0382, -0383: distrusted
intermediates and emSign roots were accepted). The frontend tooling moves to
vite 8.3.4 (launch-editor advisories in vite 8.2.0-8.2.3, dev server only)
and vitest 4.1.12 (GHSA-qxg6-89mg-rqv8, GHSA-xp26-3454-mjgp in its mocker,
test runner only).
- An Exec line awaiting a human's approval is visible before the run and can
be approved where it is used (KT-1398). Since KT-1017 a run value handed to
a program Kronn does not check alone (e.g. `aws`) is refused until a human
approves the line, but a saved Quick Exec used as a CollectApiData source was
invisible to the workflow's readiness, and its refusal said « approve this
step in the editor » without naming it. Now:
- the workflow page lists each such Quick Exec in its approval panel, with
a one-click approval (stored on the Quick Exec, the explicit confirmation
in the button);
- the readiness and the workflow list count it as a human approval;
- the Quick Exec library shows a « To approve » badge, and the Quick Exec
and Exec forms say what is asked and why above the checkbox;
- the run's refusal names the Quick Exec and where to approve it.
An agent still cannot approve: the server refuses it.
- LiteLLM image generation works (KT-1214). Kronn sent images to
`/v1/images`, a route a LiteLLM proxy does not have, and reported the 404 as
"image response carries no b64_json payload"; it now uses
`/v1/images/generations` with the OpenAI body (checked with
`vertex_ai/gemini-3-pro-image`). A provider `usage` that states tokens but
no price no longer records a 0 $ cost. A failed generation's bubble names
the model that ran and offers « Relancer… », which opens the Assets
Creator with the same prompt and kind. Opening a media room from the New
discussion entry no longer stores an empty first message.
- A comment on an arbitration card whose workflow step has ended is answered
by the room's own agent instead of nobody. It used to go to the step's
closed CLI session and wake no one ("personne ne lira cette réponse
maintenant"), so questions asked back on a framing room's cards got no
reply. The agent's reply answers the comment, and the question stays
pending for the next run; an answer or a refusal still waits for that run
and wakes nobody, and a room without a native agent keeps the old notice.
- Confluence links read from Jira no longer come back as `***spaces/…`
(KT-1331, the rest of KT-1249). The Atlassian plugin declares
`CONFLUENCE_URL` as public configuration, like `JIRA_URL`: it was masked as a
secret in every API response, so a workflow that rewrote a Jira description
wrote the masked links back. The registry definition updates installed
plugins at the next start.
- An audit no longer lets a fabricated `deps:` or `url:` provenance reach the
Stack table in the default (warn) mode (KT-1308). A `[src: deps: …]`
citation the injected dependency watch does not hold, and a
`[src: url: …]` citation of a page the run's agent never fetched, are now
blocking in the always-on documentary check, whatever the anti-hallucination
mode. The step is retried with feedback telling the model to write
`not verified: no source` instead of a « latest stable » it did not read; held
and fetched citations, and `not verified`, pass. A page counts as fetched when
the agent sent it to a fetch tool, opened it in a browser (`browser_navigate`,
`navigate_page`), downloaded it with `curl` / `wget`, or was handed it by a
documentation tool (the URLs a Context7 result names, so the citation the
Context7 prompt asks for is not refused); a leading `www.` is ignored. This
holds on the default ACP route too: the tool trace of a call now carries the
URLs it opened, read from the whole input before the 2000-character excerpt
cuts it (a long `WebFetch` prompt, a `curl` at the end of a long command), and
the URLs a Context7 result named (Claude and Codex adapters; a native ACP
agent when its update names the tool and carries its output). A native ACP
tool is a fetch by its `kind: "fetch"` too, even when the runtime retitles it
to its URL. A run whose agent reports no tool inputs (an HTTP model), that used
a sub-agent, or that called a documentation tool whose result Kronn never saw
leaves its `url:` citations unchecked, never blocked. A result that was seen
and named no page (Context7's `resolve-library-id`, which comes before every
`query-docs`) is not a blind spot: the trace carries a `result_seen` flag, so
the normal resolve → query flow keeps `url:` citations checked on the ACP
route as on the direct one. A trace keeps up to 256 URLs per call within
24 KiB; a URL over 1000 characters is dropped, never cut. The URLs kept are
put in one canonical, safe form, the same on the stored and the cited side:
no scheme, no userinfo (`user:pass@`), the value of a credential-style query
parameter masked as `name=***` (the name is judged after percent-decoding, so
`%73ig`, `access%5Ftoken`, `api%2Dkey` or a doubly-encoded name are masked
too; non-secret parameters such as `page=2` are kept), vendor token shapes
redacted. A signed page still matches its citation, a page with another
`page=` does not, and no secret is stored. The shell-download rule is
conservative: only a real `curl` / `wget` invocation that is fully understood
counts as a download, and only an `http(s)` address that is a destination of it
(a positional argument, or `curl --url`), through `sudo` / `env` / `xargs`, a
`;` `&&` `|` chain, or `sh -c`. A URL that is printed (`echo curl <url>`),
quoted, in a `#` comment or a heredoc body, or the value of an option
(`curl -d` / `--proxy` / `--referer` / `-o <url>`, `wget -O <url>`) is text,
not a fetch; a command with a heredoc, a quote left open, or an option the
reader does not know credits nothing, and so does a display-and-exit mode
(`curl --version` / `-V` / `--help` / `-h` / `--manual` / `-M`, `wget
--version` / `-V` / `--help` / `-h`), which transfers nothing. The options of
a wrapper (`sudo`, `env`, `xargs`, `timeout`, `nice`, `nohup`, `time`,
`command`, `exec`, `stdbuf`) are read with their values (`xargs -I curl
printf …`: `curl` is a replacement token, `printf` the command); a wrapper
option the reader does not know credits nothing. A citation of an uncredited
URL is blocked with the "not verified" feedback. Only the full audit
pipeline checks the run's provenance: a partial audit does not.
The pre-step baseline and the blame ledger of the documentary gate (KT-1306)
use the same run-aware analysis: an unbacked citation already there is in the
baseline and does not block the step, one the step wrote blocks it and is
charged to it. The parenthesised-citation check of KT-1322 now reads a
document without Kronn's own partial-step banner, whose quoted gate reason
(`[src: file: path:line]` turned round and cut at its limit) was refused as a
`(src: file: …)` the step never wrote.
- An audit agent that outlived a backend stop no longer keeps writing after
boot settled its step, and a resume no longer warns that « the sources
changed » when they did not (KT-1324). Agents run through the ACP adapter
(Claude and Codex by default) have no pid recorded on their run, so boot had
nothing to stop: it settled the step as « it had written nothing » and the
orphan then edited `docs/` for minutes. Every process an audit run starts now
carries the run's tag in `KRONN_TASK_DISPATCH_ID`
(`audit-run:<instance>:<run id>`, the variable `core::dispatch_reaper`
already stops task workers by; the instance is a hash of the data directory,
so a bench backend on a copy of the database never stops the live agents),
whichever route launched it. Boot stops what carries the tag of one of its
own runs (the recorded group first), and only then settles the steps that
never ended; a run something of which survived the reap, or could not be
looked for (on Linux, a process of the user's own that `/proc` will not
show), is left unsettled rather than judged on a file still being written.
Agents started under the former tag (`audit-run:<run id>`) are looked for
too, so an upgrade does not let one write past a settlement.
Every launch that takes snapshots or rewrites docs (full audit, partial
refresh, revalidation, including a revalidation that only re-anchors
citations) first stops the agents of all the project's open runs and
settles their unfinished steps, and refuses to launch while one may still be
alive. The refusal names the process and how to stop it (`kill -9 <pid>`);
when Kronn cannot read the process table (a restricted `/proc`) it says so
and offers an « I'm sure none is running: continue » action, on the audit
timeline and in the revalidation panel, that repeats the launch with
`accept_unverified_agents: true`, the human's word that none runs, which
never covers a process Kronn saw. The flag is a human-only API field: no MCP
tool carries it. The refusal's SSE `error` event carries a typed `error_code`
(`agents_unverified` for the unreadable table, `agents_still_running` for a
process that was seen) on every path, and the UI reads that code, not the
message text; a retry never resends the flag, even after a confirmed launch
that failed again. The
sources' print a run records is now read once Kronn's own
install has written its root `AGENTS.md` pointer: taken before it, it
differed from the next run's and every resume of a freshly started audit
warned.
- An audit TD card no longer fails for the whole batch when one TD cannot be
written (KT-1348, GitHub #258, reported by tarussio). A batch is now partial
by design: every TD that can be recorded is, and the card stays pending,
narrowed to the ones that were not, each named with its reason on the card.
Once the sheet is repaired, answering again applies only those (no TD is
written or accepted twice) and the validation resumes after the last one. A
card where nothing could be recorded stays pending whole and names each TD.
The reasons live in their own `td_errors` field on the card (the agent's
`context` is never cut or mixed with them, even at its 4000-character limit),
are replaced by each new partial, and disappear once the card is answered;
their history stays in the partial receipt messages.
A sheet whose status lives only
in `audit_history`, and a TD only the index lists, are resolved; and
answering one card no longer makes a later row-only TD of the same
validation unresolvable (the index resync keeps such a row as a bullet,
which is now read back).
- HTTP agents (LiteLLM, OpenRouter, NVIDIA, Ollama…) obey the same access rule
as CLI agents (KT-1346). Their native tools ran in process without the bridge
token's gate: one could create, edit, block or read the planning tasks of any
project, save a Quick API or Quick Exec in another project or move one there,
and read any media job. Their run now holds a bridge token, and each tool
that touches a Kronn object is decided and scoped by the same code as the
route it mirrors: its room's project, the room's grants for the room's own
agent, the validated workflow scope for a step, nothing more for a worker.
A creation with no project lands in the room's project, as for a CLI agent,
and a project room's agent no longer edits a general workflow or Page. A
room without a project reads the general resources; its agents (CLI and HTTP
alike) create general Quick Prompts, Quick APIs, Pages and workflows only
once a human granted it « General · write » in its Scope settings, behind a
typed confirmation, shown in red in the header and revocable (migration 270);
they then edit only what the room created, recorded at creation (migration
271), and every other general resource stays a human's to edit. A tool with
no access rule is refused. A CLI agent may now name a media connection by its
alias or name, as the media route already allowed.
- Retrying a step, or resuming an audit, from the UI keeps the run's tier
(KT-1320). The card's picker defaulted to `reasoning`, so the retry of a
Haiku (economy) audit ran on Opus. The run now records its tier and
connection (migration 268, `audit_runs.tier` / `connection_id`); a retry or a
resume sends the run's agent, tier and connection, and the panel shows the
tier it will use, until the human picks another. A resume request that names
no tier takes the run's, not Reasoning.
A connection is inherited only by the same agent; if the run's connection
was deleted since, the agent's default takes over and a warning says so,
also when the client echoes that connection id back (the UI leaves it to the
server while its picker is untouched). The run's agent and tier are kept
whatever its connection does: a connection list still loading or failed to
load is "unknown", not "deleted", and a deleted connection (also a Custom
one, or one whose provider has another connection) shows a note before the
launch — the run goes on the agent's default connection, never on another
agent. The full, partial and revalidation launches all run on the choice the
panel shows. A pick is forgotten when the project or the run to continue
changes.
- A finding that a file exists or is tracked can be cited without a line
(KT-1320): `[src: file: .ssh/key:exists]` (or `:tracked`) passes the
documentary gate for a text file, so a tracked private key or a
`yarn-error.log` no longer has to be cited `:1`, which invited quoting a
secret. A bare path for a text file is still refused — a claim about its
content needs a line — and the refusal now names the presence form.
`:tracked` is checked against the git index (`git ls-files`), not only
against the disk; a file that exists but is not tracked is refused.
- A bridge-token 403 for a discussion outside the agent's scope now says why
and what to do (KT-1320), instead of reaching a Vibe/ACP agent as a bare
"the MCP tool returned an error": ask the human to attach the discussion, or
use one of the project's discussions.
- The tech-debt review keeps the sheet valid, keeps the "why" and counts the
debts the card counts (KT-1309). Saving a decision appends its
`audit_history` entry with the indentation of the list it joins, so a
standard YAML parser (Ruby, `serde_yaml`) still reads the header; the typed
reason is the entry's note for Confirm, Defer, Reject and Keep on purpose
(the fixed note only stands in for an empty one) and the field's label says
so. The review lists every sheet the card counts, including the `CQ-*` and
`PERF-*` detail files an audit wrote without front matter (severity and
status from their index row, else Unknown): a Critical or High one blocks
the validation gate until it is settled, and settling one gives it the
front matter and `Status` line it lacked. The generated tech-debt index
lists them too, so their severity survives a resync. The impact is rendered
as Markdown and a sheet with no verification criterion says so. Card and
review read one set of debts (`td_index::debt_entries`): settled ones
(rejected, kept on purpose, or marked resolved in the index of a sheet with
no status) drop out of the card's count and show as settled in the review,
the block header shows the same active count, and a `TD-*` row of the index
with no sheet file yet is listed and can be decided (the decision writes its
sheet from the row). The history writer also handles a trailing comment on
`audit_history:` / `metadata:` and a flow-style `metadata: {…}` without
duplicating the key (a flow mapping that already holds an empty
`audit_history` gets the entry in that key, written once), reads a `#` inside
quotes as text rather than a comment, refuses an `audit_history` that is a
non-empty flow list rather than corrupting it, and keeps a new reason typed
the same day for the same status instead of dropping it. A project with only
the central index (no `tech-debt/` folder yet) can decide a row that has no
sheet: the first decision creates the folder inside the docs root, and a
`tech-debt` link leaving it is still refused.
- Revalidation after an interrupted audit (KT-1312). A refresh or a
revalidation is no longer refused once every failed step of the interrupted
run is accepted (`remaining_failed: []`): a resume would only finalize it.
A resume never runs an accepted step again, whatever blocker its document
still holds; only `redo_steps` does. The docs a step rewrote are
re-fingerprinted as soon as the step ends, so a chain of resumed runs leaves
none with the fingerprints of an earlier audit. A doc a failed step left
under its partial banner stays in the revalidation plan (its owning step, or
a manual line) and keeps the badge raised, though relevance sees nothing
wrong with its citations.
- Kronn no longer cuts a text inside a `[src: …]` marker, and the detector
gate retries (KT-1323). The TD index cut a long title at 160 characters
inside a citation; the cut opener then swallowed the next `]` of the index,
the end-of-run gate blamed step 8, and the regenerated index failed again,
for ever. Every Kronn truncation that can hold a citation (index titles, TD
task text, banner and stored reasons, the briefing excerpt) now goes through
`truncate_outside_markers`, which stops before a marker the limit would
split. The detector disposition gate no longer fails step 8 outright: it
gets the bounded retry of the other gates, with feedback naming the signals
to dispose of. The hygiene detectors (`_blank`, zero-tests, template
checks) skip generated, vendored and framework scaffolding files
(`generated/` dirs, root-level `coverage/` / `Pods/`, minified bundles,
Symfony `web/config.php`, `.gitattributes` `linguist-generated` /
`linguist-vendored` with last-match-wins and negation), so they no longer
force TDs; the security detectors, the Docker/CI inventory and a team's own
bans still see every file, so a `.gitattributes` line can never hide them.
- Agents now learn when and how to ask for a human arbitration (KT-1329).
Claude Code cuts the `kronn-internal` MCP `instructions` at 2 048 characters,
and the `kronn-question` guidance started at character 1 985 (cut to a single
sentence, no schema) or, on a first session, at 2 438 (cut entirely). It is
now a self-contained bullet with a valid inline example right after the intro
line, followed on a first session by a compact FIRST CONTACT line that points
to `kronn_intro` (also inside the cut), and a Python test pins both
onboarding states. A workflow step that can `disc_append` gets the bullet too; a spawned
task worker is told why it cannot post a card and to ask its principal. The
card is also named in the `disc_append` description, the `disc_join` next
steps and the project template, the native prompt triggers on any decision
the human must arbitrate, and the `structured-questions` skill keeps
`{{var}}:` for facts only. To keep `templates/docs/AGENTS.md` under its
800-word budget, two "Do NOT" lines were dropped from its § 2 DO NOT (new projects
no longer get "Load all T2 files at once — max 3, pick what you need" and
"Skip the test policy — see § Project parameters and § 4"); the test policy
itself is still stated in § 4 and the Do list.
- The documentary gate no longer refuses what Claude Code's Read tool shows
(KT-1322). A newline-terminated file of N lines is numbered up to N+1 there
(the last line is empty), and a range ending at N+1 was refused as outside
the file; it is now accepted by every range check (the gate, the
anti-hallucination verifier, citation drift and the bundle repair), through
one shared helper. The gate also stops suggesting what it refuses: a
comment after a non-numeric line (`docs/AGENTS.md:Focused on x`) is no
longer split into `[src: file: docs/AGENTS.md:Focused]`, an empty file gets
no `1-1` range, and a secret-looking file (`.ssh/…`, `.env`) is never
offered a range to cite. A parenthesised `(src: file: …)` citation, which
nothing verified, is refused with the bracketed form to write; a broken
relative link names the path it resolves to and the link that exists
(`../tech-debt/TD-1.md` from `docs/` → `tech-debt/TD-1.md`). Review
follow-up: citation drift hashes and evaluates a range ending at N+1 (it
reported it « removed » on the next re-audit), a range that starts on the
empty line N+1 is refused everywhere, the verifier no longer rounds an empty
file up to one line (`empty.txt:1` is refused), a text file given without a
line is offered `path:<line>` instead of a bare path, and the secret-looking
list now covers `id_ecdsa`, `id_dsa`, `*.p12`, `*.pfx`, `.netrc`, `.npmrc`
and `.pgpass`.
- The dependency watch no longer reads only `node_modules` (KT-1325). Without
an install, the version in place comes from the lockfile — `package-lock.json`
/ `npm-shrinkwrap.json` (v1, v2, v3), `yarn.lock` (classic and berry),
`pnpm-lock.yaml` — or `composer.lock` for Composer, so a checkout with a
lockfile no longer shows « declared only ». The source (lockfile vs
installed) is carried in the snapshot, the audit prompt's dependency table
and the `deps:` citation verdict, and the overview names the lockfile. Every
manifest the watch cannot settle (missing binary, network, parse error,
timeout, nothing installed) carries its reason in the snapshot, the audit
prompt block and the overview, instead of a bare « Error ». KT-1279's
workspace handling is unchanged. Review follow-up: a reason quotes what the
tool printed, so credentials are masked before it is stored, shown or put in
a prompt (URL userinfo and query, `_authToken=`, bearer headers, `ghp_` /
`npm_` tokens); a root lockfile resolves a nested project only when the
root's `workspaces` globs (or `pnpm-workspace.yaml`) list it; a Composer row
read from `composer.lock` is labelled « from composer.lock », no longer « not
installed ».
- A project validated a minute ago no longer reads « Validé, modifié depuis »
with docs to re-check (KT-1319). Relevance judged Kronn's own TD sheets as
claims: a `Next step` section (and `Remedy` ones) is now a proposal, and a
path said missing right where it is cited (« the Jenkinsfile references the
missing `config/x.yml` », « `config/x.yml` is missing from the repo ») no
longer counts as a claim that the file exists. A « missing » elsewhere in
the sentence does not exempt it, and `Mitigation` / `Recommendation`
sections stay checked. The validation also stamps the docs
no citation anchors as re-read, so their steps are not queued again right
after it with no change to the code or the docs.
- The audit's citation gate now says how to fix a citation (KT-1307). A path
that only resolves under the docs root or next to the citing doc gets
« paths are repository-root relative; did you mean `docs/AGENTS.md:94`? »,
and an out-of-range line reports the file's line count. Ranges accept the
en and em dash, and a non-numeric line says « not a line number ». A bare
path to a text file no longer tells the model to drop the claim: it proposes
the whole-file range `path:1-N` (N filled in) to cite an absence. The dash
normalisation lives in one place shared by the verifier, citation drift and
the documentary gate, so `path:17–23` is no longer fabricated under enforce.
Hint paths are canonical and only name a file that holds the cited lines; a
`#symbol` anchor is accepted after a range; a whole-file range backing a
claim that is not an absence raises a non-blocking `whole_file_citation`.
- After an accepted template v2 merge, the audit starts and the merge offer
ends (KT-1310). Accepting files now records them as Kronn's in the folder's
`.kronn.json` (committed with them, with the template install), so once the
merge branch is integrated the audit gate no longer refuses the folder as
"files Kronn did not write"; the team's sources stay theirs. The leftover
merge worktree and branch are removed when the integration is noticed (on the
next merge start or full audit), and a worktree with nothing left to review
no longer reads as a merge in progress. `briefing.md`, `.kronn.json` and the
run state are not docs to commit nor sources; a resumed or later plan no
longer reads the files an accepted merge wrote as sources. A fresh clone of a
v1-documented repository (entry file opening on `kronn:doc-version` and
Kronn's heading, no `.kronn.json`) is Kronn's. Kronn's v1 template text
(`How to fill`, `Examples (good shape …)`, `{{PLACEHOLDER}}` rows, the v1
spec file) is left in place instead of copied as team content, copied
headings nest under the target's own, a plan on the remote base warns about
local docs it cannot contain (worded by the UI in the user's language), and
the pending-integration refusal carries
`error_code: template_merge_pending_integration`. Git failing to tell
whether the merge branch is integrated (e.g. a dangling `origin/HEAD`) counts
as not integrated: the branch is never deleted by guess and the audit refusal
stays.
- Installing the template, then launching the full audit, is no longer refused
as "files Kronn did not write" (KT-1299). `POST /install-template` now records
the folder as Kronn's (`template_installed_at`) unless it already held a
human's docs, so the template's own files are never counted as foreign. The
briefing's "move to legacy" choice now moves a human's docs even when their
entry file carries Kronn's signature (it used to skip silently and leave the
launch blocked), and keeps `briefing.md` and `.kronn.json` in place. The
partial audit and the revalidation hold on a human's docs the same way the
full audit does (`docs_folder_choice_required`). The install records every
file it wrote in `.kronn.json` (`kronn_written`, one reusable
`record_kronn_written`): beside a human's docs those files are never moved to
`legacy/` (only the human's are), and they never make the folder Kronn's —
not even next to an earlier recorded audit. A recorded file counts as Kronn's
only while its sha256 still equals the recorded one: a file the human rewrote
(or a hand-crafted `.kronn.json` entry) is theirs again, counted by the gate
and moved on the choice. The install refuses a corrupt `.kronn.json` before
writing anything, and records a renamed template file under its new path.
- Revalidation no longer reads a remediation proposal as a claim (KT-1313). A
TD sheet's `Remediation` / `Fix` / `Next steps` section, or a
`- **Remediation**:` / `- **Suggested fix**:` line (and the text under a
label that closes its line), names files still to be created: they are no
longer judged by whether they exist, so `helpers/lib/plugin-factory.js`
does not turn its sheet `no_longer_relevant`. A section left with nothing
else to verify says why in its proposed correction.
- A stopped audit step keeps what it consumed (KT-1313). Its tokens join the
run's tally and its reported cost is recorded (both pipelines); a Claude
step's turns are summed as they arrive instead of keeping only the largest,
the final `result` line replacing the sum; and `total_tokens_so_far` never
goes down (it read 630, 877, then 346).
- The default-root audit golden no longer depends on the day it is run
(KT-1317). The name of a `validated-originals/<hash>.bak` file is the hash
of bytes holding the audit date; the golden now names it by the hash of the
date-masked bytes (KT-1278). A new test replays the same audit 40 days
earlier and requires a byte-identical snapshot, with no non-volatile entry
relaxed.
- Orchestration live-test findings (KT-1316). A worker's reply is priced with
the model that served it (`claude-haiku-4-5-20251001`), not the tier alias
the discussion stores (`haiku`), which names no rate: its cost is no longer
« unknown », and the delivery report shows the cost and tokens of the
worker's replies (nothing when one reply is unpriced, never a partial sum).
The « Delivery accepted » report is posted when the integration lands, not
at approval: a delivery bounced by a merge conflict or a red validation never
shows one. The orchestrator's room messages (worker brief, review request,
escalation, control offer, send-back, timeout alerts, hand-offs, campaign
gates) follow the principal discussion's language, English or French,
instead of always French; a few fix hints and a handful of rarely-seen
notices are still French. The Stop guide now says a clean worktree is
removed by default (`remove_if_clean`) and the branch is kept. The scanner no
longer logs a `resolve_host_path … contains '..'` warning per worktree: a
worktree registered with a relative path is resolved against its
registration. The MCP bridge script is looked up next to the running binary
and in the checkout it runs from, never at the path of the checkout it was
built in.
- Audit step 8 accepts a Dependencies row that states its source in any
of its forms (KT-1295). The coverage check demanded the literal
`source used:`, so a row backed by `[src: deps: …]` citations, or written
`source: …`, was refused and failed the step after the retry. It now passes
with `source used: …` or `source: …` (any case), a well-formed
`[src: deps: <manager>/<package>@<date>]` citation (the citation gate still
checks it against the watch table), or `not verified: no source`; a row
with no source still fails, and the refusal names these forms. The
`scanned — nothing substantiable` outcome also takes an en-dash or `--`.
`resource used:` no longer passes for `source used:`. A `deps:` citation
whose manager label holds a `/` (the npm label is `JS / TS`) is now
verified against the watch table instead of being read as manager `JS`:
every citation the snapshot emits verifies against it.
- The Full audit's documentary gate blames the step that introduced a blocker
(KT-1306). A document no step targets (a tech-debt detail sheet, a workflow
page) used to be charged to the founding step, so a resume re-ran the most
expensive step for another step's file. Kronn now diffs the blocking
diagnostics around each step (`.kronn/audit/document-blame.json`) and the
final gate, and a resume, redo the introducing step; the baseline is taken
before any write of the step, the cross-agent path's included, so what that
path writes is charged to its step. A step is also blocked
only by diagnostics absent before it ran: those already there are reported in
a `documentary_preexisting` event instead of failing it.
- A doc's citations are no longer lost to a truncated marker, and a citation
of an unknown kind is no longer judged as a file (KT-1311). The partial
banner cut its reason at 240 characters, sometimes inside a quoted
`[src: …]`, and the scanner then read the rest of the doc as one marker:
no fingerprint was recorded, so drift went unseen. The banner now writes the
reason with round brackets, and the scanner closes an unclosed marker at the
next `[src:` or blank line and carries on. `[src: command: git log …]`
(a kind on a closed list of non-file kinds: `command`, `shell`, `test-run`…)
is unverifiable and counted in `unverified_count`, not a missing file, so
relevance stops calling its section outdated. Any other word (`fil:`,
`path:`, `source:`) is not a free pass: a path-like text is still verified as
a file and is fabricated when missing, so the enforce gate keeps blocking it. Line references written in
backticked prose are not re-anchored; only markers are (documented in
`docs/conventions/agents-md-format-v1.md`).
- An ApiCall no longer masks a plugin's public config in the data it hands on
(KT-1249). The base URL (`https://acme.atlassian.net`) was scrubbed as a
secret since 0.14.3, so a Jira description reached the next steps with
`***/browse/EW-1` links, and a PUT would have written them back broken. A
config key declared `public` by a registry plugin (Jira's `JIRA_URL`) and a
plugin's base URL when it is a bare origin (scheme, host, optional port, no
path) are now left intact. Everything else stays masked, including any URL
with a path (webhook URLs hide their secret there), whatever its key name:
tokens, passwords, auth headers, the decoded Basic pair, every other config
value, in the data, summaries, errors and logs. An imported bundle or a
custom API cannot declare a value public. The URL decision is made on the
raw text: a secret hidden behind dot segments (`…/SECRET/..`,
`…/SECRET/%2e%2e`) no longer passes for a bare origin. A value declared
public is checked too, even with no `/`: user info, query, fragment, `;param`
and non-ASCII characters keep it masked (`https:user:SECRET@host`,
`host;SECRET`).
- A review-wait timeout no longer locks the principal out of the review
(KT-1293). The watchdog used to move the unreviewed delivery to `Escalated`
while its alert told the principal to call `task_exec_review`, which only
accepts `AwaitingReview`. The execution now stays `AwaitingReview`
(`recovery_reason` `review_wait_timeout`, the clock disarmed so the alert is
posted once): the principal can still approve (integration runs normally) or
request changes (the worker gets the comment) on the unchanged delivery, and
`task_exec_status` keeps naming `task_exec_review`. A total-duration deadline
that expires on a delivered attempt takes the same road
(`total_duration_timeout`, one alert, provider run left alone) instead of the
silent `Escalated`. Because the row is no longer `Escalated`, an overdue
review no longer puts the campaign in `AwaitingHuman` nor blocks scheduling
(the escalated counts and `sync_campaign_human_gate` no longer see it).
- An audit step run through the ACP route runs instead of asking the human
what to do (KT-1278). An ACP session has no system channel, so the context
and the step reach the model as one message, and a step prompt that ended on
a reference block (the runtime versions table, the briefing, the project
rules, the detector signals, the linked repositories) was read by a small
model such as Haiku as context to answer: it replied « What would you like
me to help with? » and wrote nothing. Every step of every audit kind, the
retries with citation feedback now end on a closing directive that names
the step and its target file, says no human answers in that turn, and stops
once the file is written. A cross-agent detection pass writes nothing, so it
gets its own closing (answer with the findings block, write no file) instead
of the step's « write the file ». A workflow step that declares read-only
repositories no longer ends on that list either.
- The version watch no longer tells the audit a JS project is up to date when
it could not look (KT-1279). A `package.json` without `node_modules` got
`npm outdated --workspaces`, which prints `{}` and exits 0 without querying
the registry, and that read « up to date » so the Renovate fallback never
ran: the workspace flags are now passed only to an installed tree, and an
empty answer for a manifest that declares dependencies but has none
installed is the new `NotInstalled` status (incomplete, Renovate decides,
listed as unchecked in the audit block). A Renovate package file with no
resolved dependency (none listed, skipped, failed lookup) no longer
finalizes as up to date either. The runtime pins also read a reusable
workflow's `nodeversion` / `node_version` input (and its `default:`), a
Jenkinsfile's `nodejs '…'` tool and node-versioned agent label, and a
Dockerfile's `FROM node:<v>` / `ARG NODE_VERSION`; when nothing is parsed the
audit block says « None parsed by Kronn » and lists the CI files present
instead of « None found ». Review follow-up: a package npm lists without an
installed version is flagged `declared_only` and shown as « not installed »,
never as the version in place; a `node_modules` holding only `.cache` / `.bin`
is not an install; a Jenkins label needs `node` / `nodejs` directly followed
by a major (`build-node-1` is a host); a Dockerfile pin needs a leading digit
and a multi-stage file marks its last stage as the runtime one; Renovate
warnings no longer drop resolved updates.
- What a task worker starts ends with its dispatch, and Vite runs in its lane
(KT-1266). A backend or a Vite started from the agent's shell tool ran in a
process group of its own and kept listening on the lane's ports after the
turn; the worker's processes now carry their dispatch id and every process
carrying it is stopped when the dispatch completes, fails, is cancelled or is
found dead, with the descendants that stayed in its session (what a shared
daemon adopted in a session of its own is left alone). A pid is signalled
only after its start time and its tag or session are checked again, so a
reused pid is never touched. `frontend/node_modules` of a lane is a real
directory of links, so Vite and vitest write their config bundle there
instead of failing with EPERM on `node_modules/.vite-temp`, with no flag;
the smoke check loads the config under the worker sandbox profile (it runs
worker-written code), and the brief says how to recover.
- Cancelling an execution no longer resets its task to `todo` (KT-1267). A
task whose work was integrated by hand (KT-1250's `3dcdbc892`) fell back to
`todo` the moment its execution was cancelled, so the roadmap counted it as
not started. The cancel now leaves the task status alone, whatever it is
(`in_progress`, `done`...): the principal decides. The cancel result and the
`task_exec_cancel` description say so, and that running the task again means
setting it back to `todo` first (`task_update status=todo`); the launch
preflight refuses any other status, so a cancelled `in_progress` task is no
longer put back in the ready queue automatically.
- A task worker's lane build runs under the worker's sandbox profile
(KT-1255). The first `cargo test --no-run` and the toolchain smoke check were
started by Kronn as the host user, so a relaunch that rebuilt after the
worker changed `Cargo.lock` or wrote a `build.rs` would have compiled
worker-written code with the host's rights. They now run under macOS
Seatbelt (Linux: bubblewrap), deny by default: writes only under the
worker's write roots and the lane's own temp dir (no global `/tmp`), no
outbound connection of any kind (loopback and Unix sockets included; only a
loopback bind), Mach lookups limited to four named services (so no
`launchctl submit`, `open` or XPC), and on Linux fresh network, PID and IPC
namespaces with `/run` and `/tmp` emptied. A host with no usable sandbox
refuses the build and says so, it never runs it unconfined: the first build
and the smoke check are then recorded as failed and the worker launches cold.
The probe also starts in a session of its own (`setsid`), with no controlling
terminal, and the profile denies read, write and ioctl on every terminal
(`/dev/tty`, `ttysNNN`, ptys, `/dev/console`) and grants no `pseudo-tty`: a
`build.rs` cannot push keystrokes (`TIOCSTI`) into the operator's shell when
the backend runs in a terminal.
- The principal's persisted validations run under that same sandbox (KT-1268).
At integration, `cargo test`, `vitest` and the `cargo metadata` that sizes
their target ran through Quick Exec as the host user in the worker's tree, so
a changed `build.rs`, proc-macro, test or `.cargo/config.toml` ran with the
host's rights. They now use the lane's profile and launcher (own session, no
terminal, lane environment, process group killed on timeout): no write
outside the worker's roots and the lane's temp dir, no connection, loopback
included. A host with no usable sandbox — no mechanism, or a launcher such
as `bwrap` that is missing or cannot apply its sandbox (a container) —
refuses the validation naming that launcher, recorded as a failed validation
and sent back to the worker, and never runs it unconfined. The lane's first
build and the toolchain smoke check (KT-1255) go through the same detection,
so on such a host they report that reason instead of `cannot start bwrap`.
A validation that needs the network or a path outside the lane
(a `git` write, an explicit `--target-dir` elsewhere) now fails there.
- A task worker's lane target (`target/wt-<lane>`) is removed with its clean
worktree (KT-1256). It lives outside the worktree, so integration and
cancellation never took it, and each one is a copy-on-write clone of the main
target that turns into real disk once the main target is rebuilt. Kronn now
stamps every lane it provisions with `.kronn-lane.json` (the execution it was
made for) and only ever deletes a stamped lane: a `wt-*` directory without
the marker (a human's or another tool's) is reported and never touched. At
boot a stamped lane is removed only when its execution is finished and its
worktree is gone; a finished execution's dirty, preserved worktree keeps its
lane. A running Cargo build always refuses. `GET /api/maintenance/lane-targets`
is the dry run (what is reclaimable, claimed, kept or not Kronn's, with
sizes), `POST /api/maintenance/lane-targets/reclaim` removes the named lanes
after re-judging each, and the boot report lists the same. Every removal is
audited as `lane_target_reclaimed` / `lane_target_refused`.
- An approve that integrates now ticks the Definition of Done items it
verified and stores their evidence on the item (the principal's
`dod_verifications`, else the worker's own `dod_status`), so a task no longer
reaches `done` with its DoD at 0/N while the evidence exists. A task with an
unticked worker item is held BEFORE the git apply (the target does not move);
the only exception is a KT-1254 `post_integration` live check, which an
integration never ticks and which keeps the task open. `task_exec_deliver` and
`task_exec_review` name the fields the backend requires (`tests_executed`,
`reviewed_head_sha`, `dod_verifications`, the string version `"1"`), and a
spawned worker's `tests_executed` schema is declared in the measured `TOOLS`
literal.
- A project's `token_override` (a raw token) is no longer returned to a
Kronn-launched agent: credential fields are stripped from every bridge-token
response.
- A task linked to rooms of several projects is visible from each of them
again (KT-1298, GitHub #248). One link to another project's room hid the
whole task, so `plan_get` answered 403 in every room and agents duplicated
the shared parent task. The links an agent cannot see are now left out of the
task instead.
- The default-docs-root golden no longer depends on the day it runs: the
compact date every step prompt opens with is masked like the dashed one.
- A Full audit installs its template from what the AI doc folder holds, not
from the project's displayed status (KT-1204). An empty `docs/`, or a folder
Kronn's docs left behind, read "template installed" and got no template, no
anti-hallucination spec and placeholder prompts for missing files. An empty
folder now gets the template, Kronn's own docs get the files they lack (never
over existing ones), and a folder of human docs is refused before the run
starts, with the same answer for the UI and for MCP `audit_launch`. A project
documented in `doc/` or `ai/` is audited there instead of in a second
`docs/` tree, and a legacy `ai/index.md` layout is asked to migrate first.
The install records the folder as Kronn's in `.kronn.json` before step 1, so
a first audit that crashed or was cancelled relaunches fresh without the
folder question; a folder with Kronn's structural markers and a Full run this
instance recorded for the project counts as Kronn's too.
- A native agent's reply to a joined CLI now reaches that CLI (KT-1201). When
the room's agent answered a CLI turn without naming it, the reply had no
target, and the bridge reported it as `withheld_by_routing` ("target another
identity"): the CLI thought the agent silent for hours. The reply is now
stored with that CLI as its target unless it names someone else, and the
delivery treats an untargeted agent reply to a session's own message as
addressed to it (`attention_required`). A native agent that writes
`@claude` while a Claude CLI is joined (the author of the turn it answers,
or the only one) now reaches that CLI instead of spawning a one-off Claude,
which used to answer as if it were the CLI. A one-off agent is told it is
not the joined CLI of its provider, whose turns it now sees labelled with
the CLI alias. Joined CLIs need an MCP reload to load the new bridge.
- The MCP `audit_status` tool reports where a project's audits really stand
(KT-1210). Its `latest` was the newest Completed run, so after a Cancelled,
Failed or step-0 Interrupted run an agent read « Completed, nothing to
resume ». `latest` is now the newest run of any status, next to
`latest_completed`, `steps_to_redo` and `lease_orphaned` (launches refused
until the cleanup action), read even while the tracker has a live entry.
`GET /api/projects/{id}/audit-latest` returns these four fields.
`audit_launch` takes `fresh` and `retry_steps`, the parameters the backend
refusal already asked for, and refuses contradictory combinations before
any call. The bridge is a long-running process: reload it to get the fix.
- An agent can open the images and videos it generated (KT-1202). They were
stored in Kronn's data dir, outside the project the CLI agent is allowed to
read, so its `Read` of its own picture failed. Generated media are now
stored like uploads: the copy the UI serves stays in the project's main
checkout under `.kronn/context-files/` (Kronn's data dir without a
project), so removing a worktree never deletes it. When the agent runs
elsewhere (a worktree, a project-less discussion's folder), it gets a
git-excluded copy under its own `.kronn/context-files/` and that path in its
prompt; a failed copy is logged and the original path given instead. Media
already stored keep working: files are served from their recorded path.
- « Recréer les fichiers du template » now ends with a next step (KT-1203).
The recreated files are raw templates, so they hold `{{PLACEHOLDERS}}`
until an audit fills them. The result lists each doc that still holds
placeholders, maps it to the audit step whose target it is, and offers one
button that runs the partial audit for exactly those steps (same lease and
saved briefing as « Mettre à jour »). Docs that no audit step targets are
listed as such, with no button. If the docs entry itself was recreated, the
project is back to `TemplateInstalled` and the result points to the full
audit instead. The confirmation says that recreated files stay empty until
those steps run. The project card shows the docs still holding placeholders,
with the same action. The count comes from `GET /api/projects/{id}/drift`
(`placeholder_docs`), so it survives a reload. It reads only the template's
own files, and `TEMPLATE.md` templates are not counted.
- A stale-sections refresh that re-verifies a section without rewriting it
now completes (KT-1207). It used to end Failed: the baseline stayed stale,
the validation revoked at launch stayed revoked, and the validate gate
refused the project until a whole 16-step audit ran. An `unchanged` step
now counts as re-verified: the run ends Completed and its sections get a
fresh baseline. Any partial run that rewrote nothing, stopped before its
first write included, restores the `validated_at` it revoked. When no
validation comes back, the re-verified sections get their own validation
discussion. An interrupted refresh now keeps what it did: the baseline is
merged for its done steps, and they get a validation discussion scoped to
them, while the run stays Interrupted. A new « Vérifié, inchangé » button
next to « Mettre à jour » marks the stale sections as still true without an
agent (`POST /api/projects/{id}/drift/acknowledge`). It refreshes their
checksum mappings, and their citation fingerprints when recorded, never
rewrites a doc, takes the audit lease, and is refused to a bridge token.
- Tech-debt and cross-agent decisions now stick (KT-1208). Answering a
cross-agent dispute card applies it under the project's audit lease:
« dismiss » rejects the sheet, « keep » leaves it active, a refusal keeps
it disputed, and the sheet's Kronn block, the step index and the central
index say so instead of « human decision pending ». The project card shows
the open disputes with a link to their discussion, and `validate-audit`
refuses while one is open. Before a run Kronn snapshots the decided
sheets; afterwards it puts back a rejected or accepted sheet the run
deleted and any decision the run changed (a confirmed sheet the run
deleted stays deleted, reconciliation reports it), and flags a new sheet
that repeats a rejected or accepted one (same file, same problem). The
cross-agent pass reuses such a sheet instead of minting an id, keeps the
previous index's undetected rows, settled sheets and `owner="human"`
sections, and reports a step that rewrote nothing as `unchanged`. The
project badge and the health score count the same active sheets: rejected
and accepted sheets and `_`-prefixed reports are left out of both. The
Kronn task of a sheet that is rejected, accepted or removed is archived
(annotated only when work on it has started). A partial refresh states, in
its stream and its UI, that it runs single-agent when the briefing asks for
the cross-agent audit.
- A favourite stays in Favoris while an agent runs or waits in it. From 20
discussions on, a live pinned discussion moved to « À suivre » and left
Favoris until the run ended. Favoris now lists every pinned discussion,
live ones first with their indicator, and « À suivre » keeps the unpinned
live and unread work, so each discussion has one shortcut row.
- Links inside project docs now open inside Kronn (KT-1200). A relative link
such as `testing-quality.md` or `../tech-debt/TD-….md` used to go to the
browser, which reloaded the app and opened nothing, since Kronn has no
router. The doc viewer now handles every link itself. A link to another doc
opens that doc in the viewer. The link is resolved from the current doc's
folder, falls back to the repository root, and never goes above it. A
`#section` link scrolls to the heading, since headings now carry slug ids.
A link to a source file, with or without `:line`, opens the source viewer
at that line. Web and `mailto:` links open in a new tab with
`rel="noopener noreferrer"`. A link to a missing doc, or to a path outside
the project, shows an inline message instead. A back button returns to the
previous doc.
- Dialogs, drawers and click targets work from the keyboard (KT-1072). A
shared `useDialogFocus` hook gives focus on open, keeps Tab inside, closes
on Escape and returns focus to the trigger; it now covers the folder
picker, the audit history drawer (now `aria-modal`), the API call log
drawer, the tech-debt review and its source viewer, « À reconfirmer »,
« Revalider l'audit » and the template reinstall confirmation. API call
log rows and the doc viewer tree take focus and open with Enter or Space;
the project card's validation, anti-hallucination and tech-debt badges are
buttons, and the card header no longer swallows Enter or Space pressed on
a control inside it. The audit history's sortable columns expose
`aria-sort` and sort from a button, and the audit timeline announces each
new step, not every tick, through a polite live region.
The template reinstall confirmation is non-modal (Tab may leave it, only
an Escape pressed inside closes it) and focus follows each step: the
report's or the error's « Fermer » after confirming, then the open button.
Escape in one of two side-by-side inline panels closes that panel. The doc
viewer's source overlay is a modal dialog, the tech-debt review and its
source viewer leave Escape to an IME composition or a control that
consumed it, and one Escape in the commit detail closes it alone, not the
viewer around it.
- One click on « Voir » in the pending-decisions banner now brings the
decision card into view. It fired a single smooth `scrollIntoView`, aimed
at the layout measured at the click; message rows use
`content-visibility: auto` and take their real height only as the scroll
reveals them, so on a long room it landed short and took up to ten clicks.
The banner now uses the room's pinned navigation, which keeps the card
centred until the rows above settle and stops on a wheel or touch. The
asset inventory's « go to message » and the in-room search's
previous/next had the same one-shot scroll and use the same pin.
- The linked-repos block of audit prompts and `docs/linked-repos.md` no
longer claim that every Kronn-bootstrapped repo has a `docs/AGENTS.md`
(KT-999): each entry states the entry point Kronn found in that repo
(`docs/AGENTS.md`, legacy `ai/index.md`, README only, nothing, not checked
for a URL, or path not found).
- Security: the citation verifier no longer marks a source outside the project
`verified` (KT-1077). An absolute citation such as `[src: file: /etc/passwd:1]`
used to pass on mere existence; it is now accepted only under a project root
(the discussion's worktree, the checkout, a declared linked repo) and then
jailed like a relative path, otherwise it is `outside_project`. Symlinks are
resolved one component at a time without following one out of the root, so
an existing and a missing file behind an escaping link get the same verdict:
nothing outside the root is stat'ed or opened. A `[src: …]` marker quoted in
inline backticks was skipped entirely; it is now verified too, green when it
resolves, and counted as unverified (amber, never fabricated) when it does
not, because docs quote the grammar itself that way (`[src: file: <path>:<line>]`).
`docs/AGENTS.md` and the citation spec say exactly this.
- A stale-sections refresh launched after a Stop now runs (KT-1188). Like the
Full audit, the partial pipeline clears a cancellation flag left by a
previous run as soon as it holds the project lease, so a Stop nobody
acknowledged no longer stops the next refresh before its first step.
- An audit no longer acknowledges the broken citations of a doc edited by hand
(KT-1187). After a Full audit or a partial refresh, only the docs a step of
that run proved it rewrote (its target, or a document its documentary
repair rewrote) get fresh citation fingerprints. A doc changed outside the
run keeps its record, so its broken citations stay « à re-vérifier » until
a validation, an acknowledgement or a rewrite checks them. A doc no step
wrote is no longer recorded by the run either.
- An audit error never stays on screen without a way out, and « Lancer
l'audit » always starts from what exists (KT-1169). A launch that stops on
an error, whether refused, cut off or offline, leaves a « Reprendre »
banner in the timeline that replays that same launch. An unreadable
timeline, recap or resume state says so with « Réessayer ». A refused
acceptance keeps its form and reason. The server now refuses a launch
without `resume_run_id` while the latest run can still be resumed, unless
the request carries `fresh: true`, the confirmed « Relancer quand même
tout l'audit » choice. A stale-sections refresh is refused in that case
too. Bridge agents get the run id to resume. When the card cannot tell
whether a run is resumable, launching waits, and a step retried alone
never turns into a whole audit. On a documented repository no launch
installs the template again. The states and their actions are listed in
`docs/architecture/audit-run-states.md`.
- `PUT /api/projects/:id/briefing` is no longer ignored once
`docs/briefing.md` exists (AUD-14): it writes the file, or removes it when
the notes are cleared, before the database column the audit falls back to.
The briefing injected into each audit step, full or partial, is capped at
6,000 characters and ends with an explicit truncation marker that points
to `docs/briefing.md`.
- A task execution's progress no longer exposes the raw name of the running
tool (KT-1124). `task_exec_status` returned it in
`TaskExecutionProgress.reason`, and an MCP name such as `mcp__github__...`
reveals the server. The reason is now a tool category (`Read`, `Mcp`,
`Kronn`...), stored as such and mapped at read time for rows written before.
The tool transcript is unchanged.
- The `db-backup.md` cron recipe now creates the backup folder as `0700` and
the copies as `0600` (`umask 077;`), with the `ls -la` result to expect
(KT-1075). A copy holds the conversation history in clear.
- The audit UI no longer gets stuck (KT-1134). Every run state offers an
action (see `docs/architecture/audit-run-states.md`). A first audit that
stopped can be resumed from the validation phase, and an interrupted
re-audit points validation to its failed steps instead of an unlinked
discussion. Relaunching the whole audit over an interrupted run asks first.
« Validation avec vous » is never « En cours » while an audit runs. A cancel
the backend did not acknowledge stays « annulation en attente » instead of
going idle. A cancel deletes only its own run's validation discussion,
never an earlier run's. A lease whose run could not be settled is retried,
then signalled with a « Débloquer » action wired to
`/api/audit-runs/cleanup`, which now spares live audits. Validation agents
are stopped when an audit starts and held while it runs, so they never
write `docs/` at the same time as the audit.
- A file cited by an agent that exists only in an unmerged worktree of the
project no longer shows as "not found" (KT-1184). Citation checks look in
the main checkout first, then in the project's own worktrees (the folders
under `.kronn/worktrees/` and the worktrees git registers for the repo,
most recently modified first, symlinked entries ignored), with the same
path jail and line checks. A path written with
`.kronn/worktrees/<lane>/` always resolves in that lane, however many
worktrees the project has. A path into another
project's worktree is refused. Each stored source check gains an optional
`origin` (`{kind: "checkout"}` or `{kind: "worktree", name}`); older rows
without it read as checkout. In the message, a lane file gets its own
neutral blue chip tagged with the worktree name, and its tooltip says it is
present in that worktree but not merged yet. The footer counts it apart
("in a worktree"), and the detail panel lists it under its worktree. Red
stays reserved for a file found nowhere. Lane files no longer count as
fabricated in the lint report, so the `disc_append` receipt stops warning
about them.
- The release body no longer mentions GitHub accounts by accident (KT-1179).
Multi-agent syntax examples in the CHANGELOG (an `at` sign followed by an
agent alias) were read by GitHub as mentions of real accounts, listed under
Contributors and notified. `release_notes.py` now wraps every bare mention
outside code, e-mail addresses and URLs in a code span, including one
that follows a lone backtick. Real human contributors, listed at release
time from the repository contributors API (bots dropped), stay mentions;
the maintainer is the fallback when the lookup fails or returns nothing.
- The desktop app now applies the saved anti-hallucination mode at startup
(KT-1149). Only the standalone backend armed it, so on desktop the setting
was ignored until it was saved again in Settings. The arming moved into the
shared startup config load, so both boot paths run it and cannot diverge.
- Creating an isolated worktree no longer edits the operator's checkout
(KT-1125). It used to create or append to the project `.gitignore` to hide
`.kronn/`, which left a new file or a modified tracked file in `git status`.
The rule now goes into the repository's local `info/exclude` (the shared git
common dir), once, and nothing at all when `.kronn/` is already ignored.
The same holds for every other private path Kronn used to add to the project
`.gitignore` (`.kronn/tmp/`, `.kronn/context-files/`, generated `.mcp.json`,
`.kiro/`, `.gemini/`, `.vibe/`, `.ai/mcp/`, agent profile files, `docs/var/`
on project status) and for the startup sweep over all projects.
A `.kronn/` line that an older version already added to a `.gitignore` is
left in place: Kronn never removes lines from your files.
- The legacy-docs check recognises the v2 template's `docs/AGENTS.md` again
(KT-1172). It compared the first line with the template heading, but the v2
file opens with `<!-- kronn:doc-version -->` comments, so a Kronn-managed
`docs/` was taken for hand-written docs.
- After a Full audit, the central tech-debt index lists every TD sheet,
including the ones the seven sub-audits wrote (KT-949). Bench runs had
indexed 7 of 35, 6 of 37 and 13 of 57. Kronn now rewrites the
`## Current list` table of `docs/inconsistencies-tech-debt.md` from
`docs/tech-debt/TD-*.md`, without a model. Each active sheet gets one row
with its title, area, severity and status, the dimension index that lists
it, and a link to the sheet. Rejected sheets and accepted trade-offs are
counted but left out of the table. A row written by hand for a TD that
has no sheet is kept, and no sheet is deleted or merged. The table is
rewritten the same way after each TD validation card. A
`Dimension coverage` row that says `N/A` or `scanned — nothing
substantiable` while sheets exist for that dimension now reads `findings`.
The row names the TDs and keeps its previous outcome. A new `unindexed_td`
warning reports an active sheet the index does not reach. The audit
summary now counts the TDs of every sub-audit, not only the central rows.
The sheets in `docs/tech-debt/` stay the source of truth.
- A second Full audit of a repository whose documentation is already up to
date no longer fails in a loop (KT-1130). A step whose agent had nothing to
change used to fail with "rewrote nothing", and "Reprendre" replayed it into
the same failure, so the run never reached Completed. When every other check
passes, the step now ends **Inchangé**: it counts as done, the run completes,
a resume skips it, and the validation discussion lists these steps in their
own section for the human to confirm. The timeline shows a neutral
"Inchangé" badge and an info toast instead of a red failure. A target that
still holds a placeholder, or that cannot be read, still fails. The outcome
is stored on the step (`audit_run_steps.unchanged`, migration 245) and in the
run's `step_outcomes_json`. A first audit of a blank repository also gets the
anti-hallucination spec its `docs/AGENTS.md` links to, so the documentary
check no longer blocks step 1. The outdated "no template to restore" advice
is gone from the step-failure toast.
- A re-audit now re-checks the documentation it already wrote against the
current code, instead of looking only for template placeholders
(KT-1132). When the target of steps 1 to 7 is already filled, the step gets a
re-verification prompt: check each fact and each citation against the code,
correct what is false or stale, keep what is still true word for word,
never touch `owner="human"` sections, and leave a still-accurate file as it
is so that the step ends **Inchangé**. Before the agent runs, Kronn lists the
target's citations that no longer resolve (deleted file, line past the end
of the file) and hands them to the step. Such a document can no longer end
unchanged: the step corrects it in the same run, or fails after three
attempts, where it used to end unchanged and block the run at the final
documentary check. A dead link or citation inside an `owner="human"`
section is never handed to the agent and never blocks a step or the run,
since no step may rewrite that section: it is reported as a
`human_section_reference` warning naming the section, and the validation
discussion lists it for the section's owner to fix. On a project already
audited, the timeline's launch
button reads "Revérifier la doc" and explains what it does before you
start. A Full audit no longer writes `docs/var/` into the project
`.gitignore`: it goes into the local `info/exclude`, like Kronn's other
private paths (KT-1125), so an audit leaves your `.gitignore` byte-identical,
or absent.
- `kronn dev` stops on Ctrl-C or TERM in every phase (KT-1145). Under bash
5.2 the backend supervisor could drop a TERM that arrived while it expanded
a command substitution, right after its Cargo build, and keep running. It
also waited for a long initial build to finish before honouring the signal.
It now uses no command substitution once its traps are set and runs the
build in the background, so the signal stops the build at once.
- In a multi-agent room, an agent launched in parallel on the same message now
knows who it is and who the message is for (KT-1153). Its prompt gives its
room alias (`@codex`), the main addressee (the message's first target), and
the sentences of the message that name it. It is told to answer in its own
role, without taking over the main agent's or speaking for it, instead of
"reply directly to the user". It also sees the whole agent thread under the
message, replies to replies included, not only the direct replies. The
deeper replies are capped at the 24 most recent and 24 000 bytes.
- An HTTP agent whose endpoint does not answer now fails in under a second
with the VPN card, and the other agents of the turn start anyway (KT-1154).
The start runs the KT-697 probe first (0.8 s budget, shared 30 s cache): a
DNS, refused, TLS, invalid-address or connect failure refuses that agent
alone, before any model call and without the runner's three retries. A
timeout or a 5xx lets the run try, so a slow proxy is never refused. Off the
VPN, LiteLLM used to show its card about 7 s after it was mentioned.
- A model the catalogue refuses before the start now leaves a System card in
the discussion instead of a toast only. The card names the model and the
cause (access denied, not found, gone from the catalogue, sign-in needed...),
gives the date the catalogue recorded it, and the step that applies. The old
"not found or access denied, choose another model" sentence is gone.
- The composer warns on the mentioned agent while you write: an endpoint the
last probe could not reach, with the reason, or a model the catalogue
refuses, with the date of that refusal. The probe runs when the set of
mentioned agents changes, not on each keystroke
(`POST /api/agents/reachability`).
- A Full audit no longer leaves its working files in your `docs/` (KT-1165,
issue #231). The citation originals and the documentary optimization report
now live in `.kronn/audit/`, which Kronn lists in the repository's local
`.git/info/exclude` and never in your `.gitignore`, so `git add docs` picks
up only documentation. Files an older version left in
`docs/.kronn-citation-originals/` move there on the next audit, after a
sha256 check of each one, and the old `docs/.kronn-document-optimization.json`
is removed, as each audit writes it again.
- A full audit can now pass on a project made of documents (KT-1164, #226).
A binary file (.docx, .pdf, image) has no lines, so it is cited by its bare
path, `[src: file: Budget 2026.docx]`, and the documentary gate accepts that
form. Kronn tells a binary file from a text file by its content, not its
extension. A line on a binary file is refused with a message that says to
drop the line; a text file still needs one. Percent-encoded links and
citations (`My%20File.docx`) are decoded before they are resolved, and a
path that leaves the project stays refused. The audit prompts, the repair
messages and the citation spec now describe the same form.
- A native agent's `disc_append` during its own turn is checked to stay in
that turn's thread (KT-1171). With no explicit `reply_to_message_id`, the
post replies to the message that started its running job, so the other
agents of the turn see it in their view of the thread. An explicit reply
wins. A post from a joined CLI, outside a running job, or naming a job of
another discussion keeps no inherited reply. The orphan reply seen in a
0.14.3 room came from a backend older than this rule (KT-1151); new tests
now pin each case.
- A Kronn backend started on its own `KRONN_DATA_DIR`, for a test or a bench,
no longer rewrites the CLI MCP configs in your home directory (KT-1144):
`~/.copilot/mcp-config.json`, `~/.codex/config.toml`, `~/.claude.json` and
`~/.gemini/settings.json` are left as they are, and the log says why. The
desktop app, `kronn start` and the Docker container sync as before, and so
does a backend whose `KRONN_DATA_DIR` names the default data directory or
that sets `KRONN_HOST_HOME`. `KRONN_HOST_MCP_SYNC=1` writes them anyway,
and `KRONN_HOST_MCP_SYNC=0` still turns the sync off.
- The same isolated backend no longer writes your API keys into
`~/.codex/auth.json` or `~/.gemini/settings.json` (KT-1162). "Sync agent
tokens" and the OpenAI and Google override toggles answer HTTP 409 with a
message naming `KRONN_HOST_HOME` and `KRONN_HOST_MCP_SYNC=1`, and change
nothing. With `KRONN_HOST_HOME` set, both files are written under that home
only; Codex used to ignore it outside Docker. The default instance syncs as
before, `KRONN_HOST_MCP_SYNC=0` included, and "Sync agent tokens" now lists
only the files it actually wrote.
- The Codex and Copilot syncs keep the MCP servers you added yourself. They
used to replace the whole `[mcp_servers]` table and the whole
`mcp-config.json`; Kronn now records the servers it wrote in
`kronn-mcp-state.json` beside each file and replaces only those. Servers
left there by an older Kronn whose MCP has since been deleted are not
recognised as Kronn's, so they stay until you remove them.
- Mistral Vibe in Kronn can call Kronn's own tools again (KT-1158). Its
permission request names only the call id, so Kronn refused every
`disc_meta`/`disc_list` call, even with full access. Kronn now reads the tool
from the update that announced the call. A call to another tool is governed
as before. A refusal now says what was refused, why, and what the user can
change. `disc_meta`, `disc_list` and `disc_load_other` refuse an unexpected
argument and point to the right tool (`disc_meta` reads only the current
discussion).
- An Agent step inside a foreach sub-workflow no longer crashes the backend
with a stack overflow (KT-1161). In a debug build each nested workflow run
takes about 1 MiB of stack, so a foreach child filled the 2 MiB default of a
runtime worker before its Agent step started. The backend and desktop
runtimes now give their threads 8 MiB, and the static tool catalogue is
built once instead of on the stack at every Agent step.
- Automation and Planning no longer stall (KT-1148). Automation re-rendered
in full on every workflow poll (every 3 to 10 seconds), and these renders
kept the browser busy for close to a second at a time on a loaded machine.
Both pages now re-render only when their own data changes. Planning starts
its first load at once instead of waiting 180 ms for typing to settle. While
a list or a detail loads, both pages show the spinning Kronn logo instead of
an empty state or the old spinner. The database-size banner on Automation
now reads the file size only (a few milliseconds) instead of measuring
every table. That measurement took 1.7 to 2.4 seconds on an 11 GB database,
and Settings still runs it, but on a connection of its own: it no longer
holds up the requests queued behind it.
- A workflow's file and line limits could miss a file rewritten at the same
size in the instant its git index was last written: on Linux the safety
snapshot copied the index with a new date, so git trusted stale file data.
- `rustls` 0.23.46 fixes RUSTSEC-2026-0346 to 0349.
## [0.15.0] - 2026-10-10
### Upgrade notes
- Workflow Security settings saved before this version now apply as stored
(KT-1043); nothing is unticked or disabled for you. Check the warning on
each affected workflow's page. A workflow with "Sandbox (Docker)" ticked has
every run refused on a native (non-Docker) install, its Cron and Tracker
runs included. One with "Approval required" now waits for a human before
every run: a Cron or Tracker trigger leaves runs waiting for approval
instead of running unattended. One with "Max files" or "Max lines" is
refused when its project has no directory or is not a git repository. A run
that had already started when you upgrade is not paused for approval when it
resumes (after a Gate, a quota wait or a restart), but it is refused by the
sandbox setting, and newly set limits measure from the point it resumes.
- Cron and Watch triggers saved before this version are pinned to `UTC`
(KT-1103, migration 242), so they keep firing at the same instants. A new
trigger without a timezone follows Kronn's timezone instead: to write a
schedule in local time, clear the `UTC` of an existing trigger and rewrite
its hours (e.g. `5-19` UTC becomes `7-21` in Europe/Paris, with no rewrite
at each DST change). `{{time.now}}` without `tz:` now renders in Kronn's
timezone; add `|tz:UTC` where a template needs UTC.
### Added
- Kronn has a timezone (KT-1103): Settings > Server, an IANA name, by
default the machine's zone (`TZ`, then the OS setting, then UTC; a Docker
container reports UTC unless the compose `TZ` is set). Cron and Watch
triggers without their own `timezone`, and `{{time.now}}` without `tz:`,
use it. A local time the spring DST change skips does not fire that day;
one the autumn change repeats fires once. The trigger editor shows the zone
in use and the next 3 firings in it (`POST /api/workflows/cron-preview`).
Tracker intervals stay in UTC.
A run renders its dates in the zone it started under, through Gate,
quota and restart resumes and in its child runs; a run pinned before this
version renders in UTC. The desktop app applies the zone at boot too.
- Live Pages: a dataset can now be emptied, re-limited and deleted
(KT-1104). `clear` is a publish write for every kind (time-series points
deleted, snapshot set to `null`, collection to `[]`), also usable in a
`PublishPageData` step without `value_from`. `PATCH
/api/pages/{id}/datasets/{name}` changes `max_points` / `max_age_days` and
prunes the existing points at once. `DELETE /api/pages/{id}/datasets/{name}`
(MCP `page_delete_dataset`) is refused, with the list of references, while a
workflow writes the dataset, the Page HTML names it or a Page button binds
to it; a human can force it from the Page, an agent cannot. The data panel
now shows each dataset's size, last write, writing workflows and whether the
HTML names it, with Empty, Delete and limit controls behind confirmations.
Changing limits and reading usage are human-only. The MCP catalogue stays
within its byte ceiling (lowered to 85,341 B).
- « Ma Todo » ships with Kronn (KT-1030): a board Page (to do / in progress /
done) over the planning tasks tagged `todo`, with add, edit, ⤒ ▶ ◀ ✓ ↺,
tags, one discussion per task, and a chevron that shows a card's
description as rendered Markdown. It is installed once at first launch,
never again after you delete it, and never over a todo page of your own;
the Pages sidebar offers to (re)install it. Its six workflows use only the
new zero-token `TaskBoard` step and `PublishPageData` (no Python, no URL),
arrive enabled, and no action is pre-approved: approve « todo-move » once in
the Page's details to save a drag and drop without its card. A drop moves
the card at once and puts it back with the reason if the launch fails. A
first-view notice, dismissible, says the page runs without an agent, on
Kronn's Tasks (Planning) and Workflows (Automation), and that any configured
agent can adapt it and its workflows through Kronn's MCP tools (blocks, new
statistics, a validation or sorting step).
Editing a task writes only what changed, so a long description is never
cut. Closing the notice is remembered by Kronn for that Page (a sandboxed
Page has no storage of its own).
Cards show tags as chips, a priority badge, a relative date, a description
preview and the linked discussion; a search bar filters the board live by
title, description or tag, ignoring case and accents.
- An open Page follows a publish at once (`live_page_data_changed` WebSocket
event) instead of waiting for its 30-second refresh (KT-1030).
- A skill can belong to one project (KT-1128). "Nouveau skill" joins the
Automation page's create menu, and a custom skill's sheet gets "Modifier";
the form takes an optional project (the page's project filter by default).
A project skill is a custom skill of the library carrying
`metadata.kronn-project`: same file, same `custom-<slug>` id, so the run
pins and every reference keep working. It is listed on its project's
AI & automation tab as a project skill ("Dans Kronn seulement" until it is
published) without becoming a default of its discussions. No other project
offers it, in its discussions, workflow steps or tab. An agent launch of
another project that names it is refused by name, before anything starts.
The tab proposes the user's global skills before Kronn's built-in ones.
`POST`/`PUT /api/skills` and the MCP `skill_create`/`skill_update` take
`project_id`: absent keeps it on update, `null` (or `""` through the MCP)
makes the skill global, and an unknown project is refused. A `skill:<slug>`
reference resolves the project's own skill and its repository skill in the
project scope (both at once is refused as ambiguous), then a global custom
skill, then a built-in; another project's skill never resolves.
- A workflow Agent step loads the project's repository skills (KT-1128). A
step may name a `repository:<project>:<slug>` skill, which a run used to
drop without a word. The run pin (KT-1096) reads it as committed on the
repository's default branch (the remote one when the clone has it) and pins
it with the other skills: neither the branch the checkout is on, nor the
run's worktree, nor a later commit changes what the step loads. A skill the
pin lacks (another project's, not committed on the default branch, no longer
used by the project, or a launch outside a run) stops the step by name. The
step editor offers the workflow project's repository skills.
- Repository profile `kronn/project.toml` (KT-920, ADR-005 slice 7).
Workflows read what is specific to a repository (validation targets, forge
labels and rules, tracker statuses and transitions, delivery workflows) as
`{{project.<path>}}` instead of hard-coding it. Kronn reads the file at the
commit of the main checkout's default-branch ref, never from a run's
worktree, so a pull request cannot change the rules of its own run, and
pins what it read with the run tree, one snapshot per project: a resume or
any child run sees the same values even if the file changes during a
pause, and the pinned revision fingerprint covers them, as does a trusted
Page action's approval: changing a profile value invalidates it, and the
run it admits pins the snapshot that was checked. The schema is typed and
refuses unknown keys, malformed files and anything that looks like a
secret, without ever quoting the file in its error; a run that needs a
missing key fails before its first step, naming it, and projects without a
profile are unaffected. A completed Full audit drafts a starter profile
when the repository has none and keeps it with the audit run, without
writing into the checkout; the audit view shows it read-only with a Copy
button. Schema and an example:
`docs/guides/project-profile.md`.
- A workflow step now delegates a plan's subtasks without an orchestrator
agent (KT-909). `DelegateSubtasks` takes a parent task, launches each ready
subtask with the worker named by its `worker:<key>` tag, in plan order,
within the step's concurrency and the plan's blockers, and waits on the
executions' durable state with no model call. Each delivery is reviewed by
the step's agent in one fresh, short session that sees the DoD, the diff and
the worker's report, and answers approve, request changes, reassign or
escalate. The diff is cut at 40,000 characters: an HTTP reviewer has no
tool to read the worktree, so past that cut it does not review the whole
change; Kronn applies the verdict and integrates approved work into the
run's branch, validations included, through the existing review and
integration code. The step ends with OK, or with ESCALATED, CONFLICT,
BLOCKED, FAILED or TIMEOUT for `on_result` to route, and lists per subtask
its status, integrated SHA, review rounds and cost. The step timeout also
bounds a review in flight: a verdict that comes back late is kept, never
applied or integrated past the deadline. Each review is recorded in the run's
durable LLM-call count before it starts, so a restart keeps the cap, and
a refused write ends the run before another review. A restarted step picks
its campaign back up: nothing is launched twice, a verdict already recorded
is applied without a new review, a reassignment interrupted midway is
applied once instead of escalating, and boot recovery no longer wakes a paid
principal turn in the step's room.
- Workflows can use a `Watch` trigger (KT-1099): on its interval, the
scheduler polls a source with a GET through the API broker (an API
configured in Kronn, or a saved Quick API; stored credentials, the API's
default headers, guarded outbound HTTP) and creates a run only when the
source changed. The stored ETag / Last-Modified go out as a conditional
request, so a 304 costs nothing; a 200 is then compared by its validators,
by a fingerprint of the body, or by a fingerprint of a JSONPath result, so
volatile fields can be ignored. A poll creates no `workflow_runs` row: the
validators, fingerprint and unchanged/changed/error counters live in
`workflow_watch_state` (migration 237) and show on the workflow card, and
three failed polls in a row show the workflow as failing. The first poll of
a source records a baseline without a run. Each served project's admission
of a change is recorded with its run, and the baseline advances only once
every project has its run, so a change detected again (a restart before
the poll was acknowledged, a project refused by its concurrency limit or
preflight) runs once per project and never twice. A Quick API the trigger
polls counts as a dependency: an agent's edit of it disables the workflow,
it cannot be deleted while named, it travels with the workflow's export,
and a human edit of it starts a new baseline. The run receives the triggering response as `{{trigger.body}}`
(credentials removed, cut at 64 KiB) with `{{trigger.status}}`,
`{{trigger.fingerprint}}`, `{{trigger.etag}}`, `{{trigger.last_modified}}`
and, in JSONPath mode, `{{trigger.extract}}`. The trigger is editable in the
wizard and through MCP `workflow_update`; as for any trigger, an agent's
edit disables an enabled workflow until a human turns it back on, and
`workflow_create_draft` defaults a Watch to one concurrent run. Watch
intervals take five cron fields (at most one poll a minute).
- Cron and Watch triggers take an optional IANA `timezone` (KT-1099), so
"7h–21h Paris" is `0 7-21 * * *` with `Europe/Paris` and follows the DST
change. Without one the schedule stays in UTC, as before, and the editor
says so. On the change days, a local time that does not exist (spring) does
not fire that day, and a repeated one (autumn) fires once, at its first pass.
- Workflow tools now say whether a saved workflow can start (KT-1138). Every
create, update, import, read and bundle returns a structured readiness
verdict (`readiness` in the API envelope, `kronn_readiness` first in agent
tool results, `readiness` in the bundle response): `ready` plus each blocker
with its workflow, step, rollback flag, kind (validation error, misconfigured
step, human approval, unsafe interpolation, missing child, cycle, collection
error), the runtime reason, the action and whether only a human can lift it.
It covers sub-workflows, triggered workflows and every `on_failure` chain,
and reads the run's own validators (the Exec allowlist, path and
irreversible-operation refusals of the main and setup lines now live in
functions shared by the run and the verdict; a line with a template is left
to the run). `GET /api/workflows/{id}/readiness` and the new
`workflow_validate` MCP tool return it on demand; `workflow_list` keeps the
`misconfigured_step_count` and `unsafe_step_count` it used to drop, zero
included, and adds `blocker_count` and `human_approval_count` (the misconfigured
count now includes rollback steps). Saving or enabling is no longer presented
as readiness, and `ready` means no known refusal, not a guaranteed run. Agents
still cannot approve a line, pin a script hash or clear its provenance
(KT-1017): the `workflow-architect` skill and the manuals tell them to fix
what they can and to list each pending approval to the human. In the
workflow view, a line that only lacks a human approval is no longer described
as a dangerous interpolation; it offers "Review and approve", and blocked
sub-workflows and rollbacks are named. The Exec step schema explains the
code/data split and the approval agent-written lines need.
- Runs that change nothing are marked, hidden and purged fast (KT-1100). A
successful top-level run is stored with `outcome: no_op` when every step
declared or proved that it changed nothing: an Exec step prints a
`KRONN_NOOP` line, an Agent step puts `"no_change": true` in its envelope,
and Kronn infers an identical Page publish, a `GET`/`HEAD` call, a Collect
step made of Quick API reads and the pure data steps. Anything else, a
worktree, a preserved branch, a dataset write that changed content, a
recorded non-`GET` call or any row pointing at the run, keeps it `changed`.
The outcome is returned by the run API and by MCP `workflow_runs` /
`workflow_run_get`. The run list hides these runs by default
(`hide_no_op=true` on `GET /api/workflows/{id}/runs` and `/runs/count`),
offers "Show runs without changes (N)", and folds a streak of them into one
greyed row, "N runs without changes since HH:MM". A no-op run keeps the
first 2 000 characters of each step output and is deleted 24 h after it
finished by the existing run retention pass. A workflow can carry its own
retention (`retention: {no_op_hours, success_days, failure_days}`, editor
card "Run retention", `workflow_update`), which overrides the global
windows per class; `0` keeps forever. Runs that own a worktree or that
something references are never deleted. Retention is human-only on a
stored workflow: an agent's `workflow_update` refuses a changed
`retention`, and a `kronn/` re-import keeps the stored one.
- Multi-agent discussions now say who is launched and how (KT-1109). While a
draft names several agents, the composer shows "N agents launched in
parallel" with one chip per agent, and the sent message's routing line adds
"N in parallel". When the first named agent is told to delegate to the
others ("@opencode lance @codex et @claude", "ask", "demande à", "fais
juger par"…), sending first asks whether that agent should orchestrate
them. Orchestrating attaches the other agents to the discussion
(`attach_agents` on `PATCH /api/discussions/{id}`, which never dispatches
them and is refused to agents' bridge tokens) and sends the message to the first agent alone, which hands off with
the existing `kronn:handoff` marker; the routing line then reads
"orchestrates @codex, @claude". The choice is offered only when agent
collaboration is on, warns when the paid handoff limit is below the number
of delegated agents, and launching everyone in parallel stays one click
away. The answer is bound to its discussion and draft: editing the text,
switching discussion or cancelling while the attachment is pending sends
nothing, and only the first choice taken runs. A native reply that names an agent in prose without launching it now
carries a discreet note saying so and why: already scheduled on this turn,
or a plain mention with no handoff.
- The orchestrate-or-parallel choice is also offered when the first named
agent is given a role or job over the others (KT-1152): "@opencode tu es
juge des blagues de @codex et @litellm", "compare les réponses de",
"arbitre entre", "you are the judge of", "eres el juez de", "评判"… The role
must reach the next agent within one clause, so a plain list, a parallel
ask ("@a et @b donnez votre avis") or "@a compare X, @b aussi" still
launches everyone in parallel without asking.
- The configuration assistants (custom API plugin, workflow and Quick API
ApiCall step) keep their conversation when closed instead of deleting it
(KT-1111). Each one is created and filed in one transaction as an assistant
conversation linked to the plugin or the step it configures (migration 226,
`assistant_conversations`; `POST /api/discussions` takes an `assistant`
context). It shows in a new Assistants section of the discussion list rather
than in Recent or the project tree, and the global search finds it. The plugin
form and the ApiCall step list their "Assistant conversations" (date, agent,
whether the last proposal was applied); "Resume" reopens the same
conversation in the assistant, and deleting one is an explicit, confirmed
action. Conversations started before a plugin, workflow or Quick API is saved
are attached to it on save, and stay attachable after a failed save: the
browser records them as owed to the saved plugin or step (by conversation id,
never by name), which lists them and attaches one on resume. Without browser
storage (disabled or cleared), such a conversation stays reachable from the
Assistants section but is no longer reattached to its plugin or step on its
own. A saved ApiCall step is known by its durable id, so renaming it keeps its
conversations. Every write into an assistant conversation (its creation, user
messages, agent replies and streamed checkpoints, edit-resend and revisions)
masks the stored credentials of the linked plugin, common token shapes, and
the form values the assistant names with a message: those are held in memory
only, per dispatch, until that dispatch's reply is written or the dispatch is
over (a queued or running one keeps them however long it lasts). The assistant
masks them on display too. A custom API's literal header values
reach the agent only for known non-sensitive headers or `${ENV.*}` references
(KT-1041). The logical export (v7) carries the links; older archives still
import. The new routes (`GET /api/assistant-conversations`,
`PATCH /api/assistant-conversations/{discussion_id}`) are not open to agents'
bridge tokens. Conversations deleted by earlier versions cannot be recovered.
- A plugin's API can now be limited to chosen agents (optionally one model
each), to local models only, or blocked, as a whole and per endpoint, from
the plugin's Access tab (KT-1026). Under a policy the broker is strict: only
declared endpoints (the spec's and the policy's) can be called, with exact
method and segment matching (a placeholder keeps the literal text around it,
so `report-{id}.json` never opens `private.csv`; malformed braces are
refused), so `/blocks/{id}/children` no longer reaches what a rule on
`/pages/{id}` protects; an encoded slash, a dot segment or a path leaving the
base URL is refused. Every request the call sends is re-decided: each next
page and each redirect hop, including one that turns a POST into a GET. The
decision uses the identity Kronn issued at launch (the bridge token carries
the agent and its model; a native agent's tools are told the model the
runner actually sends), never the request; a call without a token is an
agent with no identity. "Local only" means an Ollama model on a loopback host
that is not a cloud model, in a discussion whose agents are all local (a
joined CLI counts as remote), or a workflow whose Agent steps all are,
rollback steps included, read from the definition the run pinned (a run
with no readable pin counts as an unknown, hence remote, audience; so does
any step type that may hand data to an agent Kronn cannot name, including
ones added later). Requests and rules are compared in one canonical form:
an encoded unreserved character is decoded (`/users/%6De` is `/users/me`),
other escapes take uppercase hex, an ASCII character the URL escapes in a
path (space, quote, angle brackets, backtick, braces) is escaped, and a
non-ASCII character (a Unicode space included, never trimmed) is its UTF-8
escapes, as the request URL is sent. A rule holding a control character
(which the URL drops or escapes) is refused, and a stored one makes the
plugin refuse every call until it is fixed. A rule is stored in that form, and two rules that reach the same
form are refused. Workflow ApiCall, BatchApiCall and
CollectApiData steps, Quick API runs and the MCP `api_call` all pass the same
gate, before any token exchange or outbound request; a refusal names the
rule and no secret. The API context, `mcp_list` and `api_endpoints` show each
plugin's and endpoint's rule. Only a person sets or widens a policy
(`PUT /api/mcps/servers/{id}/access-policy` is not open to bridge tokens). A
plugin without a policy behaves as before. The `api_call` description no
longer claims undeclared paths are refused when they are not. Database
exports move to v7 and carry the policies; an older export keeps the local
ones. On a hybrid plugin the panel says its MCP connection keeps its own
access, outside this restriction.
- A Live Page action can run without its validation card once a human has
approved it from the Page's details (KT-1029), for example moving a card
between columns of a Todo page. Only workflow actions whose steps are all of
a type known to run no agent (any other or future type is refused), without skills,
profiles, directives or Quick Exec data sources, without a typed or
project-environment value and in the Page's own project are eligible; the
panel says why the others are not. The approval is bound to a fingerprint of
the action block, the Page's project and the workflow's shared revision
identity, and falls for good, with a visible notice, as soon as the block
changes or leaves the Page, or the moment its workflow or one of its Quick
APIs is written, disabled or deleted, even if the change is undone. Each
trusted launch needs a proven live click, runs only under the approval it
was claimed with, and its run is admitted, created and pinned to the
approved definition in one transaction, so a later edit or revocation never
makes it execute anything else; it is rate-limited per row and per action
and appears in the button's history. Withdrawing an approval is one click and applies to the next click.
Neither the Page nor an agent can approve: the routes are outside the
bridge-token list and refuse an agent identity; `page_get` shows agents which
actions are trusted.
- The waiting reply bubble now shows what the agent is doing from the first
message on, for every agent (KT-1108). Before the first word it lists the
real startup phases with their times: a native ACP runtime being launched,
`initialize`, `session/new` while the project's MCP servers start (with
their count), model selection, a Claude or Codex CLI starting with its own
MCP servers, then the prompt sent and the wait for the model. Once the model
works, the phase follows it (thinking, using a tool, writing) and an
expandable list shows its tool calls by category, as in the audit Details
panel. When the agent is silent, the bubble counts down the inactivity
delay in force (a startup phase's own bound, the model's delay, or a tool's
wider one), and a stop by Kronn is announced in the same bubble. Every phase
comes from the code path that observed it; frames carry categories, counts
and durations only, never a tool's name, a path, a URL or agent text. They
travel on the local WebSocket only, coalesced per run (a phase or tool change
at most every 0.5 s, a mere sign of life at most every 5 s), and a frame
older than the one shown, or one after the run stopped, is ignored. A page
opened or reconnected mid-run reads the runs in progress from
`GET /api/discussions/{id}/run-progress` (not open to agents' bridge
tokens); reading it never restarts a run's silence. A reply always shows its
latest attempt, and a run is shown only in the bubble of its own dispatch or
turn. HTTP agents (Ollama, LiteLLM, OpenAI-compatible) report each request
with its first-token deadline (each retry attempt with its own, no countdown
during the backoff), the wait for the model's next answer after a tool,
reasoning and keepalive chunks as activity, a Kronn tool from the moment it
starts, and their own inactivity stop. A streamed HTTP run now owns its
silence bounds the way an ACP run does (first token, then a fresh bound once
the headers come, then each chunk), so the discussion and workflow
consumers no longer arm a second text-only timer on it; the global deadline
still holds.
- HTTP agents (Ollama, LiteLLM, NVIDIA, custom connections) can find, create
and update Pages: `page_list`, `page_get`, `page_create`, `page_update_html`
and `page_add_dataset` join their native catalogue and call the same handlers
as the MCP bridge, within the same scope: project-less Pages and the current
project's Pages, never another project's. A discussion agent gets them in
the full catalogue (or in the `pages` family with `KRONN_TIERED_TOOLS=1`); a
workflow Agent step gets them too, while Planning mutations stay out of its
catalogue. Asked to find a Page, create one and draft the workflow that
feeds it, a local `qwen3.8:27b` did all three; the workflow published to the
new Page on its first run.
- Before a multi-agent discussion or room starts, Kronn checks every selected
agent in parallel without calling any model (KT-1107): installed, full access
on for the native ACP agents, signed in where the CLI has a status command
(`claude auth status`, `codex login status`, `kiro-cli whoami`), and, for the
native ACP agents, a real `initialize` + `session/new` with no prompt, in the
project directory, so a project MCP server that blocks the start is caught
within the launch's own 90 s bound. The form shows ✅/❌/❔ with the reason;
if an agent is not ready, nothing starts until you fix it, remove it, or
launch anyway, and the room shows the same state in a compact notice. Results
are cached for 3 minutes per agent and project, and dropped when the agent's
settings, full access, key or the project's MCP servers change. A check that
cannot be made without the model (OpenCode, Gemini, Copilot and Vibe sign-in)
is reported unknown, never ready. `POST /api/agents/readiness` is not open to
a launched agent's bridge token.
- Settings → Agents shows the command Kronn actually runs for each agent (the
installed CLI's path, or the `npx` command it falls back to) and the version
that command reports, with a warning when it is the `npx` fallback. A run
that falls back to `npx` records the command and version in its provenance,
never in the agent's output.
- A Live Page's slug can be renamed: from the slug pill in the Page header
(with a preview of the new link), through `PATCH /api/pages/{id}` with
`slug`, or by an agent with `page_update_html({page_id, slug})` (MCP or
native tools). The new slug
follows the creation rules (lowercase ASCII, unique across all projects). The
former slug keeps opening the Page, so links, workflow steps and API calls
that name it still work, and it stays reserved for that Page until the Page is
deleted: no other Page, import included, can take over its old links.
Exports name the Page by id where a workflow still used an old slug, so the
bundle imports cleanly (KT-1098).
### Changed
- "Invite" in a discussion now copies the `kr-join-…` token alone as soon as
it is minted, and the modal says so (KT-1146). Three tabs replace the "Full
handoff" checkbox: Token only (default), Simple instruction and Detailed
instruction, each with its own copy button. When the browser refuses the
clipboard, the modal says the copy failed and leaves the text selected for a
manual copy. The token itself is unchanged. The copy is started inside the
click, so the desktop app's WebKit view accepts it even when minting is slow.
- A task description in Planning, and in the discussion plan panel, now shows
as rendered Markdown by default, with the same safe renderer as discussion
messages (no raw HTML, safe links), instead of raw source (KT-1137). A "Raw"
switch at the top of the block shows the source, where it can be edited in
Planning; the chosen mode is remembered in the browser. Switching never
drops an unsaved edit, and saving is unchanged.
- `{{previous_step.data…}}` and `{{steps.<name>.data…}}` no longer read an
older step's data after a step that produced no envelope (KT-1105). To read
an earlier producer, name it with `steps.<producer>.data`, which works as
long as that producer has not been re-run without an envelope.
- The "Run without confirmation" panel of a Page is now one collapsed summary
line, for example "4 to approve · 1 approved · 22 not eligible" (KT-1141).
"Details" opens the list: eligible actions first, with their approve and
withdraw controls, then the others grouped by reason, each group collapsed
with its count. The browser remembers whether you left it open. Approving
and withdrawing work as before.
- Kronn's built-in skills are now listed apart from the user's (KT-1140). On
the Automation page's Skills cards and on a project's AI & automation tab,
the skills in use split into "Skills Kronn" and "Mes skills" (custom,
project and repository skills). Both screens use one shared rule and the
same badges: "Projet", "Dépôt" and "Pas synchro". "Pas synchro" shows only
on the project tab, the one screen that knows the sync state. Unused skills
stay folded below, as before.
- A pull request page now lists only real CI results (KT-1159). Adding a
label or editing the description no longer starts CI Tests and CI Build,
whose skipped jobs used to hide the results of the run that really tested
the change. Those events start CI Verdict instead, which publishes only
`ci-quality-gates` and `ci-build-gates`, repeating the verdict of the run
for the same head and base. A label added after a push re-runs that run, so
its real results appear. CONTRIBUTING explains which checks to read.
### Fixed
- In a multi-agent room, a native agent that posts through MCP during its own
run no longer starts agents a second time, cancels itself or runs out of turn
(KT-1151). Its post now belongs to its turn: it replies to the message that
started the turn and carries the turn's job. Agents already scheduled on the
same human message are not started again. The bridge sends the turn it was
launched for, and Kronn checks it against the running job. After a restart,
the agent's own post no longer cancels its turn, but a newer human message
still does. Turns in one discussion now run in the order of their messages
(then creation). Before, whichever turn won a race ran first.
- Under Vibe, Kronn's bridge now knows its discussion: `disc_meta` no longer
fails with "no disc bound" (KT-1082). Vibe starts MCP servers without its own
environment, so Kronn declares the discussion id and backend URL for the
bridge in `session/new`. The bridge token and a workflow step's capability
are not declared, because Vibe saves that declaration in its session files:
they go in an owner-only file (mode 0600, in a 0700 directory) that the
declaration names and that is deleted when the session ends. The other
native agents are unchanged.
- A workflow Agent step on OpenCode, Vibe, GitHub Copilot, Gemini CLI or Kiro
whose full access is off now fails at once with
`native_full_access_required` and a message naming the agent and the fix
(KT-1086). It is not retried, not read as a quota limit, and the
`on_failure` chain still runs. The workflow's readiness check lists it as a
blocker before the run, and the step's agent picker badges such agents
"Needs full access" until the setting is turned on.
- Kronn's action card names the row in words a Page gives it (KT-1030). A
Page can add `data-kronn-binding-labels` next to `data-kronn-bindings`; the
card shows those labels instead of raw selectors such as
`__col_in_progress__`, with the selectors still in the tooltip and in the
resolved values. Labels are display only: they never reach the server, and
the action block and its trust approvals are unchanged. « Ma Todo » labels
its moves in four languages (task title, position, column). In a narrow
column the card now puts its title on its own line and wraps the row
instead of cutting it.
- `agent_list` no longer reports a named HTTP connection as reachable just
because an address is saved (KT-697). Each connection now gets a short
network probe (an unauthenticated `GET /v1/models`, 3 s max, all connections
in parallel, results cached 30 s). A connection whose host does not resolve,
refuses, times out, fails TLS or answers 5xx is `reachable: false`, not
`available`, with the `endpoint_unreachable` code the legacy LiteLLM family
already used. A new `connectivity` field tells `verified`, `unreachable`
(with `unreachable_reason`: `dns`, `refused`, `timeout`, `tls`,
`http_status`, ...) and `unverified` (no probe, or a result older than 60 s)
apart. The connection stays listed with its id and media slots either way.
The field never contains the address or a credential.
- The desktop app no longer blocks allowed third-party players in Live Pages
by its own headers (KT-1123). It sent
`Cross-Origin-Embedder-Policy: require-corp` and
`Cross-Origin-Opener-Policy: same-origin` on every response, so the webview
refused any frame whose site does not opt in to that policy, whatever the
allow-list said. Both headers are gone, as in Docker, which never sent
them; the frame policy (`frame-src`) is unchanged. They only made
`SharedArrayBuffer` available, so the local voice workers now run on a
single WASM thread in the desktop app, as in Docker. Proven: in a native
macOS WKWebView, a synthetic player without COEP/CORP is blocked under the
old headers and loads without them, and text-to-speech still produces
audio without isolation. Not yet checked: the packaged app, a real player
(YouTube…), Windows WebView2. Local speech-to-text currently fails in
WKWebView with or without these headers (KT-1143), unrelated to this change.
- The `kronn-internal` MCP bridge no longer hangs on a large request
(KT-1139). A `workflow_update` of more than 256 KiB, such as a 1 MiB
workflow, killed the bridge's input reader, and every later call waited
forever. A request may now carry up to 8 MiB. A larger one is refused with a
`request_too_large` error that runs nothing, and the connection keeps
working. If the reader stops on an unexpected error, the process exits
instead of hanging. The limits are listed by `bridge_info` and in
`initialize`. Large workflows are now edited one step at a time:
`workflow_get` takes `step_name` or `step_index` to read one step, and the
new `workflow_update_step` changes one step through the same save as
`workflow_update`, with every authorship, approval and enable rule unchanged.
If another save changed the workflow after the edit read it, the edit is
refused with a conflict and nothing is written.
See `docs/operations/mcp-servers/kronn-internal.md`, "Transport size
contract".
- The header of a Page in the Artifacts view no longer puts the Page id under
the "Open in new tab" button (KT-1141). The id stays with the title and
ends in an ellipsis when space runs out; the buttons share one height and
wrap onto their own row when the view is narrow, and below 480 px "Open in
new tab" shows only its icon.
- A CLI listening in a room with `disc_wait_for_peer` no longer goes deaf
between polls (KT-703). The bridge slept the server's pacing delay after
each quiet 15-second poll: 40 s while a human was active, up to 480 s in a
quiet room. A message posted during that sleep waited for the next poll,
so after a calm period a reply could take more than 8 minutes while the
session still looked present. The bridge now starts the next poll right
away, so it listens continuously and a new message arrives within one poll.
The server's pacing values are unchanged. A CLI reading a wake result is
also no longer told to rotate its session in a shared room. The
`session_budget` verdict now appears only in a delegated task's execution
room, where rotating is the intended response. In a shared room, it had
been read as an order to stop listening.
- On macOS, the `caffeinate` that keeps the Mac awake during a run no longer
outlives Kronn (KT-1127). It is now started as `caffeinate -i -w <backend
pid>`, so macOS ends it when the backend exits, however it exits (crash,
SIGKILL, update, desktop quit, normal exit while a run holds the lease).
Before, an orphaned `caffeinate` stayed alive forever and the Mac never
idle-slept again. It also no longer inherits descriptors the backend itself
inherited without close-on-exec.
- The workflow Security settings now take effect (KT-1043). They were saved
but nothing read them. "Sandbox (Docker)" refuses to start a run outside a
container. "Approval required" pauses each run before its worktree, its
hooks and its first step, until a human approves or rejects it from Kronn;
an agent's bridge token cannot decide it, and a sub-workflow child with this
setting fails instead, since it cannot pause. "Max files" and "Max lines"
compare the run's working tree, by content, with its state before the
workflow's hooks ran (changes already there are not the run's; hook changes
are). The run is measured after each step, its declared artifacts included,
and once more after the `after_run` hook. Files git is told to ignore changes
to (assume-unchanged, skip-worktree) are still measured, deletions
included; a flagged path already missing when the run started (a sparse
checkout) is not counted. The step that goes
past a limit fails and the run stops there for good: no quota wait, no
`on_failure` chain, changes kept for review. Such a stop in a sub-workflow
ends its parent the same way, and a foreach dispatches no further item. A
step's `---STATE:` output can no longer write Kronn's own `__kronn.*` run
state, and a recorded starting state that cannot be read stops the run
instead of being taken again. These limits need a git working tree; without one, or without a
project directory, the run is refused. The editor and the workflow page say
which stored settings this host would refuse. An agent that loosens these
settings on an enabled workflow disables it, as for any other execution
change.
- Unticking every Security option in the workflow assistant is now saved
(KT-1044). An all-cleared panel used to send no settings, so the old ones
came back on reload.
- The reply bubble's Logs panel follows the same no-leak rule as the run
progress (KT-1120). A tool call is logged by its category only (`→ Read`
when it starts, `✓ Read` when it ends), never by its name, input, file path,
URL or command. Kronn reduces the line before sending it, so the client never
receives those details. When the agent writes to its stderr, the panel shows
a fixed `⚠ Diagnostic` notice once, never the text: it can name paths and
URLs. A failed run's error message still carries that text, and the tool
calls recorded in the transcript are unchanged.
- A `PublishPageData` write fed by an `Exec` stdout no longer publishes a
truncated document (KT-1105). An `Exec` step a publish reads now keeps up to
2 MiB of output instead of 100 KB; a stdout cut at that limit, or one that
looks like JSON but does not parse, fails the publish step with an explicit
message and the Page keeps its previous data. A JSON stdout is stored as the
JSON value rather than a string (pages that `JSON.parse` a string keep
working). The Exec envelope now flags `stdout_truncated` and
`stderr_truncated`, and Exec output is scrubbed of the project's GitHub
token by value.
- `Exec` and `ApiCall` no longer read a whole command output or JSON response
into memory before cutting it (KT-1047). An Exec step drains stdout and
stderr as they come, keeps its limit plus a small margin and counts the
rest, so a command writing tens of MB stays bounded and its output ends with
the truncation marker. An ApiCall JSON response is read chunk by chunk and a
body past 16 MiB fails the step with an explicit message instead of being
loaded; an error body is read only as far as its excerpt needs.
- A workflow run now executes only the revision it started with (KT-1096).
Its first execution freezes the workflow and every Quick Prompt, Quick API,
sub-workflow, skill, directive and profile it can load, sub-workflows' own
dependencies included (migration 227). Gate and quota resumes, restarts,
rollback steps, sub-workflow children and BatchQuickPrompt fan-out (chain
prompts included) read that revision, so an agent's edit made while a run
is in flight never reaches it. That covers the skills, directives and
profiles a fan-out child starts with (a pinned run gets them inline in its
prompt, never through the shared native skill and profile files a project
sync can rewrite mid-turn),
the `ref:` targets and `{{ref:…}}` values the run resolved, and a rollback
Agent step's Quick Prompt. The edit still disables the workflow for the
next runs, and a resume of such a run still needs a human. A dependency
deleted during the run fails its step with a message naming it. A run whose
workflow an agent's change disabled between its launch and its start does
not start. A run started before this version is pinned at its next
execution.
- A workflow run that ends partial, fails, is cancelled or is stopped by a
guard no longer deletes a worktree holding uncommitted or untracked work,
in itself or in a worktree created inside it, and neither does the boot
purge of finished runs (KT-1096). The worktrees and their branches stay,
the run's last step says where, and a later start removes them once clean.
A clean set is removed without `--force` and never forced: when git
refuses (work written after the check, a submodule), it is kept with the
reason. A fully successful run still discards what it left
uncommitted, as before: once every step finished, those files are its own
scratch.
- A Gate with `gate_checkpoint_before` no longer commits the operator's
checkout (KT-1042). The checkpoint commit is taken only in the run's own
worktree (workspace isolation). In shared mode no checkpoint is taken and
the Gate message says so, where Kronn used to `git add -A` the project
directory and commit the operator's unstaged and untracked work on their
branch. The checkpoint records the work under review: "Request changes"
re-runs the target on top of it, after checking that the worktree is still
exactly that commit. It no longer runs `git reset --hard`, which could only
discard commits made by someone other than the paused run.
- The anti-loop guards of a workflow run no longer restart from zero at
every resume (KT-1046). Step visits (`loop_detection_max_revisits`, and
`{{iter.<step>}}`), the fires of each capped Goto edge, the total iteration
safeguard and the `max_llm_calls` count are stored on the run. They carry
over a Gate decision (approve or request changes), a quota wake-up and a
restart, so a loop through an auto-approved Gate stops where the same loop
without a Gate stops. A step replayed after a quota refusal or a crash is
not counted as a new visit, and a call refused for quota does not count
against `max_llm_calls`. The LLM calls of sub-workflow children are recorded
on the root run as they happen (a new `tree_llm_calls` column that only
grows), so a crash in the middle of a foreach no longer forgets what the
children that already finished spent. If that count cannot be written or
read, the run stops before its next call instead of continuing on a total
it cannot trust, and a child whose calls were not recorded is not taken as
done. That stop is terminal: the run ends Failed without its `on_failure`
compensation steps, quota wait or recovery rule, since each of those could
spend further calls. This covers calls that finished: a child still running when the
backend dies is started again on resume, so its interrupted call can be
billed twice. Runs paused before this version
rebuild their counters from their own step history and that of their
sub-workflow children.
- `agent_list`, `task_exec_prepare`, `task_exec_launch` and the tier pickers
now read the launch preflight's own catalogue decision (KT-860). A tier the
preflight would refuse is listed with the reason (`model_unavailable`) and
is not launchable; a tier whose model disappeared shows the model that runs
in its place. A run refused at preflight keeps the refused model, the reason
and the next action in `last_error`, instead of "model catalogue preflight
refused this model".
- Codex: Kronn now reads the whole `model/list` of the installed CLI, every
page and hidden models included (KT-667). A tier model absent from that
complete listing keeps its tier and launches with a warning shown
beforehand: a "Not listed" label on the tier choice (detail on hover), a
`model_not_listed` warning in `agent_list` and `task_exec_prepare`, and a
notice at the head of the run. It is never refused or replaced on that
evidence, since a listing does not prove account access; only a refusal the
provider actually returned blocks a model. A listing cut short or a failed
refresh leaves the evidence unknown, and models an older visible-only
listing had marked disappeared are available again. When Codex answers
"requires a newer version of Codex", the error names the model and the
detected and latest known CLI versions instead of repeating "upgrade".
- A workflow Agent step refused for a provider quota or session limit no longer
fails the run and loses the steps before it (KT-811). The run waits in a new
`WaitingQuota` status, shown as "Quota" rather than as a failure, and resumes
at the refused step after the announced reset (Claude's
`resets 9:40pm (Europe/Paris)`, a `Retry-After`, Codex's `try again in …`,
an ISO reset), even across a backend restart. Without a usable reset, past
the workflow's time limit or after repeated refusals, the run is parked
with its reason and a "Resume" button; it can be cancelled while it waits.
Automatic resume covers linear root runs only: a sub-workflow child, a batch
or an `on_failure` compensation is not resumed, and a child ends `Failed`
with its step marked as quota.
- On a native macOS backend, an agent CLI installed under a `KRONN_HOST_BIN`
directory is run instead of being skipped for `npx`: the Darwin host-binary
guard now applies only inside a container. In a container, resolution goes
on through the `PATH` after a skipped host copy, so a later Linux copy wins.
- A workflow step with a `TypedSchema` output on an OpenAI-compatible provider
sends `strict: false`. The envelope schema does not satisfy OpenAI's strict
subset (every object `additionalProperties: false`, every property
required), so a provider enforcing it refused the request.
- A reasoning-effort override that differs from the catalogue only by case
(`High` for `high`) is still refused, and the refusal now names the
catalogue's spelling. When the catalogue entry itself carries stray
whitespace, the refusal says so instead of asking to retype the value.
- The custom API helper and the workflow API call helper now show an Apply
card when the agent wraps the `KRONN:APPLY` marker in its own code fence,
puts it on the first line inside the json fence, or writes it in another
letter case, with LF or CRLF line endings (KT-1110). Both helpers share one
parser, which only accepts a JSON object and ignores the word used inside a
sentence. When a proposal block cannot be read, even next to a valid one,
the chat shows a notice with a Retry action and the raw JSON instead of
dropping it silently. The system prompts in the 4 locales now say that the
marker is a bare text line outside any code fence.
- Live Pages: the browser itself now refuses to frame a site that is not
allowed, in desktop, native and Docker alike (KT-1115). Every app document
carries `frame-src` and `child-src` set to the app plus exactly the allowed
sites (same rules as the Docker gateway: no wildcard, an `http://` site only
with its `https://` counterpart, 16 KiB bound). An allowed site that
redirects, or navigates its player, to another origin is blocked before any
request reaches that origin. The desktop sets it on the documents its
embedded backend serves (its Tauri CSP is null), native mode through a Vite
plugin reading the same backend route, Docker through the gateway as
before. Removing an allowed site now takes its players down at once in every
open tab, a visible one included, through a local WebSocket event that
carries no origin; a tab that was disconnected re-reads the list when it
reconnects. A site allowed after a page was opened shows a "Reload to show
this content" notice until that page is reloaded, since a document keeps the
policy it was served with. Because that policy still admits a revoked site,
revoking any site takes down every player of the open tabs, not only the
revoked site's (an allowed player may have been redirected to it), until the
page is reloaded; a change also hides the players until the new list is
read, so a failed read never leaves them running. Each document carries the
exact sources its policy was served with (`<meta
name="kronn-served-frame-src">`, written with the header in every mode), and
the page compares later lists against it, so a site revoked before a Page is
first opened is still treated as revoked; without that marker no player is
drawn. An allowed `http://` site whose `https://` address is not allowed is
never framed; its placeholder now says to allow it over https instead of
asking for a reload. This lifts the two 0.14.3 known limitations on
embeds, except the one below.
### Known limitations
- An allowed embed site can itself embed other origins inside its own page:
Kronn controls only what the Page frames directly, redirects included
(KT-1115).
## [0.14.3] - 2026-10-07
### Upgrade notes
- OpenCode, Vibe and GitHub Copilot now start only with full access. To keep
using them, tick their full-access box in Config › Agents › <agent> (the same
applies to Gemini CLI and Kiro).
- P2P federation is now off by default and the upgrade never turns it on. If
you use it, enable "Accept P2P connections" in Settings › Identity, and add
the LAN or Tailscale names you open Kronn from to the frontend origins list
there, or their WebSocket is refused.
- Imported workflows (`.kronn-workflow.json`, bundles, agent proposals) and
workflows an agent creates or changes arrive disabled. Review them in the
Automations banner and re-enable the ones you trust.
- Migrations 220 to 223 run at startup and take about a second (220 to 222
measured at 1.2 s on an 11.5 GB database).
- Secrets leave `config.toml` on the first start and move to the encrypted
database (see Changed and `docs/operations/key-management.md`).
### Known limitations
- OpenCode, Vibe, GitHub Copilot, Gemini CLI and Kiro require full access: no
restricted mode can be promised for them. A per-launch isolated runner is
planned for 0.15.
- Inbound P2P is off by default. While it is on, a peer is identified only by
its invite code, so anyone who knows an accepted contact's code and can reach
the port could impersonate it; pairing secrets are planned for 0.15.
- A workflow run already in flight can still load a Quick API or Prompt that
an agent changed during that run; revision pinning is planned (KT-1096).
- A SubWorkflow foreach read can still be fooled by a process that mutates the
worktree while it runs (KT-1055).
- With localhost trust on (auth off, or `auth_strict_localhost = false`), a
local process without a bridge token is indistinguishable from the user;
strict localhost is the mitigation until positive human authorization
(KT-1034).
- P2P mirrors joined before 0.14.3 have no recorded host: join them again with
their code to resume syncing.
- Agent text and tool metadata in transcripts, audit streams and run records
stay visible to the project's humans and to agents holding a bridge token
for that project; per-agent filtering is planned for 0.15.
- On Windows, stopping a native ACP agent ends the agent's own process but not
the MCP servers or tools it started; owning the whole tree (a Job Object) is
planned for 0.15.
- An allowed embed site can redirect to or frame another origin: Kronn checks
only the embed URL's starting origin; browser-enforced frame-src across
redirects is planned for 0.15 (KT-1115).
- Removing an allowed site reaches an open tab at its next focus (KT-1115).
### Added
- Live Pages can show third-party players (a Suno or video embed, for example): a Page writes
`<div data-kronn-embed="https://player.example.com/embed/x">` and Kronn draws the player itself,
only for sites listed under Configuration > Artifacts > External content. The Page's sandbox and
CSP are unchanged. Artifact bundles declare the sites they embed, and the import asks before
adding any. Adding a site, directly or during an import, needs local access or the API token even
when authentication is off. Works in Docker too: the gateway lists exactly the allowed sites in
the app's `frame-src` (an `http://` site only when its `https://` counterpart is allowed too), and
a newly allowed site plays after a page reload. Contributed by Barbé Rémi.
- When a dangerous Exec step needs a manual fix, its warning now offers a ready prompt to copy and hand to an agent: it names the workflow, each step and value to fix, and the safe pattern (KT-1017).
- Each project now decides whether its agents receive a GitHub token (KT-1006,
D2). A "GitHub" row on the project's Overview, and a chip in the discussion
header, show the state: not connected, available but off, connected through
your gh login, or connected through a token pasted for the project and
stored encrypted. "Connect GitHub" asks for confirmation, shows the scope
GitHub reports for the token (OAuth scopes, or the repositories of a
fine-grained token, or why it could not be verified) and recommends a
fine-grained token when the gh token reaches every repository. Turning it
off applies to new launches; agents already running keep what they received.
- A running audit step now has a "Details" toggle listing the agent's latest
actions, newest first: each one only as a fixed category (read, search,
edit, command, web, MCP tool, Kronn, plan, other) with its age. Tool names,
paths, commands, URLs and titles are never shown or kept, whatever the agent
or runtime. The same categories replace the tool name and target in the live
tool chip, the audit stream's events, an HTTP agent's label and a workflow
step's live activity; an activity a workflow run stored before keeps its
count only when read, in workflow runs and their shared-run projections.
Agents' bridge tokens get the counts, not the list. Logs and traces of a
tool call the model made up name its category, never the name it chose. The
open state is remembered per project for the session. Known limitation: the
boundary is the project. Agent text and tool metadata in transcripts, audit
streams and run records stay visible to the project's humans and to agents
holding a bridge token for that project; filtering them toward other agents
of the same project is planned for 0.15.
- The Agents page shows a one-time notice listing the agents that really run
with full access, whether by setting or forced in Docker, with the risks and a
link to the switches (KT-975).
- Under Docker, Codex always runs without its own sandbox, which cannot start
inside the container; the full-access switch now shows that state as locked
and always on instead of "Restricted" (KT-975).
- Run retention (KT-984) is opt-in on every install, new ones included:
nothing is emptied until you choose a window in Settings → Database, where
30 days is the suggested value. While it is off, the Automations page and
Settings → Database show a banner with the database size and a button to the
setting; it can be dismissed and comes back past 2 GB. Once on, every 6 hours, in chunks that
leave the database free between them, Kronn empties the step outputs of
workflow runs finished before that delay and keeps the runs, their
steps, statuses, timings and tokens. Runs in progress, paused or
interrupted, batch and compare runs, runs that still own a worktree and runs
referenced elsewhere (child runs, discussions, ratings, live pages,
questions, room activity) are never touched. A run's worktree path is
cleared as soon as the worktree is removed, so a finished run does not wait
for a restart to become eligible. The delay is set in
Settings → Database ("keep forever" turns it off). Settings → Database also
offers to compact the database, which gives the freed space back to the
disk and reports the size before and after; on a generated 4 GB base the
file went from 4.1 GB to 833 MB. The opt-in deletion of old runs
(`run_retention_days`) now follows the same rules in chunks instead of one
boot-time delete that also removed comparisons and their ratings, and
deleting a workflow is refused while one of its runs is live, paused or
interrupted with a worktree.
- Full access is now a visible, accessible switch on each agent card, with a
risk dialog before it is enabled, and the setup wizard has an Access step
that offers it agent by agent, off by default (KT-975).
- The plugin catalogue offers MySQL / MariaDB (KT-996, #217), through the
community server `@benborla29/mcp-server-mysql`: host, user and password,
with an optional port and database, read-only unless a write flag is set on
the server.
- A skill can declare variables in the Claude Code format: an `arguments`
list used as `$name` in its body, an optional `argument-hint`, and Kronn's
label, default and control for each one as JSON under
`metadata.kronn-variables`. Kronn reads, validates and writes them back
unchanged, and such a skill shows a "Variabilisé" badge in every skill list,
its hover naming the placeholders (KT-906).
- The project card says when the branch has lost its audit evidence while
another commit still carries `docs/.kronn.json`, names that commit and its
branches, and restores the file on request (KT-993). A Full audit completed
on this Kronn instance now counts as audited when the branch has no evidence
file, and the audit timeline lists the audits the file records (another
instance, an attestation, legacy evidence) with their date and provenance
instead of staying empty. The timeline loads in one request instead of up to
thirteen.
- The audit timeline shows what each step cost, as the agent reported it
(Claude Code's `total_cost_usd`, OpenRouter's `usage.cost`), summed over the
step's attempts, and the audit's total: exact when every step reported,
"≥ x $" naming the unknown steps otherwise. A step whose agent reported no
cost shows Kronn's estimate where one can be made (see the token figure
below) and "cost ?" otherwise, never 0; an HTTP step where any response came
without a cost is never shown as a partial sum. Each run records the model its agent served,
or the configured one labelled as such, and `step_done` carries the step's
cost (KT-997).
- Saved workflows whose Exec step puts a value into inline code (`bash -c`,
`python3 -c`, `node -e`…) are now flagged with an "unsafe interpolation"
badge, and such a step no longer runs: the run stops at that step, after
the earlier steps, with an error naming the step, the value and the fix,
whatever started the run (KT-1017). In the workflow, "Suggest a fix" shows
the rewrite as a diff, the value moved into a separate argument (`$1`,
`sys.argv[1]`, or `process.argv[1]` after a `--` that ends Node's options),
and applies it only when you click
Apply. When no rewrite is provably equivalent (heredoc, single quotes,
part of a longer string), it says a manual fix is required and gives the
recipe. Editing something else in such a workflow keeps working. The
inline Quick Exec sources of a CollectApiData step are checked, counted and
fixable the same way.
- Workflows name other resources by slug: `{{ref:<kind>:<slug>}}` in any
templated step field resolves at run time in the run's project, then the
global scope, for workflows, Quick Execs, Quick APIs, prompts, skills (the
repository's included), plugins and pages, and an unknown reference fails
its step before launch. Sub-workflow, trigger, Quick Prompt and Quick API
ids are written to `kronn/` as `ref:<kind>:<slug>` and stored as local ids
at save and import, so a workflow published from one machine and imported
on another runs its targets without editing.
`GET /api/resources/resolve?ref=&project=` resolves one for a script
(KT-917).
- One workflow can serve several projects: `project_scope` lists them (or
all), the project is resolved when the run is triggered (the request's
`project_id`, the launching discussion's project, one run per project for
a cron, the project linked to the tracked repository for a tracker), the
run executes in that project's worktree, and concurrency limits count each
project's runs apart. The workflow editor chooses this project only, all
projects or chosen projects (KT-851).
- Each workflow run has its own artifacts directory, `{{run.artifacts_dir}}`:
Exec steps write screenshots, logs or reports there, and a Claude Code or
Codex Agent step whose prompt names it reads it without being able to
write or delete anything in it (the `read_only_repos` policy). It is
removed with the run (KT-910).
- A workflow Agent step can declare its tools in `agent_settings.tools`:
Claude Code built-in tools and Kronn tools by name, or none. A declared
step starts with exactly those, without the other MCP servers, the skill
listing or the Kronn catalogue and its instructions; an undeclared step
launches exactly as before. The step editor and `workflow_step_schema`
expose the setting, validated at save (KT-908).
- Each agent launch gets its own bridge token instead of Kronn's admin token:
a random value held only in memory, bound to the launch's discussion, task
execution or workflow run and their project, revoked when the launch ends
and forgotten on restart. It opens only the routes the `kronn-internal`
bridge calls, only for resources in its scope (ids in the path, the query
and the body are checked), never a route that returns or moves a secret,
and effects are logged with its id. A workflow it triggers without naming a
project runs for the room's project when the workflow serves it (KT-851),
and is refused otherwise. Ids are resolved like the handler resolves them
(`KT-12` included) and an unknown id is refused; lists, searches and lookups
only show the token's project; an API call runs for the token's project with
a config that project can see; deleting the room kills its token; the
WebSocket bus refuses it. The bridge reads `KRONN_BRIDGE_TOKEN`
first. Loopback requests without a token keep today's trust until the
per-action human proof ships in 0.15 (KT-1006).
- An Exec step that runs a repository script can declare the script and the
modules it loads, as paths relative to the repository of the workflow's
home project (KT-918). Saving refuses a path outside the repository, a
missing file or a symlink that leads out of it, and records each file's
content hash, which enters the workflow's approval fingerprint; a save made
by an agent never records a hash, so only a human approves. Before the
command runs, Kronn checks every file against its approved hash and copies
it into the run's artifacts directory; the command runs from that copy,
with the run's checkout in `KRONN_WORKTREE`. A file changed since approval,
even by one line, stops the step before anything runs and the error names
it; the step editor shows each file as approved or changed and re-approves
the current content on save. A workflow published to `kronn/` names the
files and their hashes, never their content. Absolute imports and packages
found outside the copy are not covered.
- Launching a workflow, you can see and change the agent, model and effort of
each Agent step, for that run only (KT-1025). The launch card, in a
discussion or on a Live Page, has a folded "Details" block that lists the
Agent steps with what is planned and the agent and model selectors; without a
change the launch request is the same as before. The trigger route, the
discussion and Live Page action launches and the MCP `workflow_trigger` tool
accept `step_agents`. An unknown or non-Agent step, an agent that is not
installed or enabled, or tools or a token budget the chosen agent cannot use
is refused before the run starts, with a message that says why. The choice
is stored on the run, so it still applies after a Gate approval or a resume,
and the step shows the agent it really ran on; the workflow is never
modified.
- API plugins can declare headers sent on every call, whatever their
authentication: Notion's mandatory `Notion-Version`, Anthropic's
`anthropic-version`, the `Accept` Heroku requires. Without one, such an API
refused every request (Notion: `400 missing_version`) and each workflow
step or agent had to repeat it. Custom API plugins edit them in the new
**Headers sent on every call** section, as a literal or `${ENV.KEY}` from
a field; a header given on a call overrides the default. The 🪄 helper and
the Workflow architect now look for such a header in the docs and propose
it. Validation refuses credentials, `Authorization`, `Host`, duplicates and
undeclared fields, and now also runs on bundle imports, which skipped the
check refusing CLI-token authentication.
- Agents no longer see an API's authentication keys, or keys already sent as
headers, listed as "pass as query params" in their API context.
- A Custom API plugin's **Test** button now works: it calls the endpoint
marked as test endpoint (a round button on each endpoint row), which must be
a `GET` without path parameters. Kronn selects the most likely one by
default (`/users/me`, `/me`, `/whoami`…, else the first such `GET`), so
existing plugins pass the test without being edited. It used to answer "No
side-effect-free authentication probe is declared" for every custom plugin,
even a working one.
### Fixed
- A native ACP agent (OpenCode, Vibe, Copilot, Gemini CLI, Kiro) that stops
answering no longer leaves a discussion silent. Opening a session now waits
90 s, enough for OpenCode's 30 s per MCP server; the 30 s limit lost that
race to one slow project server, and the turn was deferred and retried
without a word. A startup phase that times out or returns an error stops the
agent, settles the turn as failed and posts a translated message naming the
phase, the error or the project servers it was starting; a stop during
startup is honoured at once. A native prompt now stops at the configured
inactivity delay, not 15 minutes, measured on every update the runtime sends
(tool calls, thoughts, Claude thinking and Codex reasoning, keepalives), not
on reply text: the discussion and
workflow timers leave an ACP run's inactivity to it, parallel tool calls
each keep their wider bound, and the delay applies as soon as the last one
ends. A project server named by a bare command found nowhere on PATH is no
longer declared to the session. A stop by Kronn's inactivity watchdog is no
longer headed by a wrong "Network error" hint.
- The launch card's step details now show the model and efforts of the
step's tier (economy or reasoning), not always those of the default tier
(KT-1095).
- An audit step retried after an attempt that did not report a token counter
no longer shows a cost estimate: the counter stays unknown in the step's
total instead of reading as complete (KT-1094).
- The taller model menu no longer runs past the bottom of the window: it
opens on the side where it fits, or the roomier one, and is never taller than
that side's room (KT-1032).
- Audit quick wins:
- Translations with a repeated placeholder (for example "2 dossiers
sélectionnés") no longer show a literal `{1}` (KT-1053).
- Two quick clicks on Confirm in the automation import dialog import once,
not twice (KT-1052).
- In Docker, the gateway now accepts config and discussion imports up to
512 MiB, like the backend, instead of answering 413 above 64 MiB; CI
checks that every backend body limit fits under the gateway's (KT-1064).
- CI runs `git diff --check` on each pull request, and `make check-diff`
runs it locally (KT-1063).
- `make test-backend` runs the whole suite CI runs, integration tests
included; `make test-backend-lib` keeps the quick library loop (KT-1054).
- A sub-workflow child respects its own workflow's concurrency limit: a
refused single child fails the step with the reason; a foreach item waits
for a free slot and is skipped only shortly before the run's timeout (as
set when each run started), with a SKIPPED signal that the run keeps. A
resumed child keeps its concurrency key, and no child is started or
resumed once its parent's cancellation has settled or its run's timeout
has passed; every guard stop, and every cancellation, settles the run's
whole subtree of children (KT-1045).
- Run retention and a bridge token's workflow list read indexes instead of
whole tables (migrations 221 and 222, KT-1048, KT-1050).
- A SubWorkflow foreach file must be a relative path inside the worktree:
absolute paths, `..` and symlinks leading out are refused on save (editor,
bundle, restore) and at run time. Foreach reads and writes in the worktree
never follow a symlink or a hard link (best effort on Windows); a write
replaces the file through a synced temporary file, keeps its permission
bits (a new file follows the umask) and refuses sockets, FIFOs and
devices. Known residual, tracked in
KT-1055 for 0.15: a read can still be fooled by a process that mutates
the worktree while it runs (KT-1038).
- The pre-migration backup now includes the writes still in the
write-ahead log, checked on the WAL file SQLite itself uses (symlinked
databases included); when another connection blocks that, the upgrade
stops and says why instead of saving a stale backup (KT-1049).
- The desktop app refuses to serve an unauthenticated API on `::`, as it
already did on `0.0.0.0` (KT-1065).
- The docs-migration success toast shows again (KT-1076).
- The CORS allowlist has router-level tests, with and without a domain
(KT-1058).
- `docs/stack.md`, `docs/repo-map.md` and `SECURITY.md` match the code
again, and the tech-debt registry lists open items only, each with its
detail file, checked in CI (KT-1060, KT-1061).
- Security: a web page the operator visits can no longer open
`ws://127.0.0.1:3140/api/ws` to read the event bus or inject chat messages
(KT-1033). The upgrade checks `Origin` against exact origins: the frontend
on the listening port (the desktop's runtime port included), the gateway
ports only under Docker, the configured domain, the Tauri webview, the dev
UI under `kronn start-dev` (which now follows `VITE_DEV_PORT` and keeps an
operator-set `KRONN_DEV_UI_URL`; Vite no longer falls back to another
port), and a new list of extra origins in
Settings > Identity for LAN or Tailscale names, checked with a full URL
parser. A page on another port gets 403, and a frontend still needs the
token when auth is on, unless strict localhost is off (unchanged rule).
The frontend now sends the token as a WebSocket subprotocol instead of
`?token=` in the URL, so no proxy log can hold it (`?token=` is still read
from older clients, and dropped from the backend span and the gateway
access log). Heartbeats are answered on their own socket only.
- Docker detection reads `KRONN_IN_DOCKER` only when it is `1` or `true`.
- Security: P2P federation is now a setting, "Accept P2P connections", off by
default and never turned on by the upgrade (KT-1033). A peer is identified
only by its invite code, so anyone who knows an accepted contact's code and
can reach the port could impersonate it until pairing secrets arrive in
0.15. While it is off, `/api/ws` refuses every non-frontend client, the
peer routes (`claim-by-token`, `fetch-file`) answer like an unknown code,
no contact is dialled, pinged or asked to resolve a join code (the join
says P2P is off), and an attachment transfer in flight is cancelled. The contacts panel says why contacts are offline
and enables P2P in one click. When it is on, only an accepted contact is
admitted, checked again at every frame in both directions and every few
seconds: deleting or refusing a contact cuts its connection. A shared
discussion travels only between its members (the contacts it was shared
with, or on a mirror its host): another accepted contact never receives
it and cannot write into it or ask to sync it, even knowing its id. A
peer's attachment must name a message of that same discussion; it is
downloaded to a staging file in the discussion's own folder, under its
full file id, and published only if the sender is still authorized at
that moment. A file served to a peer is checked again right before its
bytes leave, and the transfer stops if the contact is revoked, removed
from the discussion or P2P is turned off. Turning P2P off also stops
connections still being set up and join-code claims in flight. A
mirror joined before this release has no recorded host: join it again
with its code to resume syncing. An unknown
code becomes a contact request (shown as "request", capped and expiring,
never dialled or pinged until you add its code) and counts toward the ban,
unadmitted sockets are capped per address, and contact addresses must be
a bare host and port, requested without following redirects.
- An API call's credentials are scrubbed by value from everything it hands on:
the step output (successful JSON included), URL summaries, errors and the
call log, through one set built from every value the call takes from the env
or the credential store, wherever it lands (base URL included: no plugin
metadata marks a value public, so a summary may show a masked base URL) plus
the resolved auth and default headers. Each value is masked raw,
URL-encoded, hex, base64 and JSON-escaped, case-insensitively from 8
characters, before any truncation; shorter ones only as whole tokens. Notify
scrubs its whole response before cutting the excerpt, a token-exchange error
loses every `${ENV.*}` its body used (the same parser as substitution, so
`${env.password}` counts), and a successful token response is never quoted.
Stored text (call log, run detail in the UI and through MCP
`workflow_run_get`, MCP run status) gets URL query/userinfo stripping and
the secret heuristics, the most that can be done for output written before.
Known limits: an endpoint that transforms what it echoes defeats any finite
list of forms, and a short secret glued inside another token stays visible
(KT-1035).
- Turning a workflow on is a human decision. A workflow imported from a
`.kronn-workflow.json` file, an accepted agent bundle (parent and children)
and an accepted agent proposal land disabled, as ADR-005 and the Artifact
import already required, so a shared Cron or Tracker trigger no longer fires
on the next tick. An agent's token can no longer enable a workflow through
create, `workflow_update` or `workflow_set_enabled` (`force` included): the
backend refuses and tells it to ask the user, and an agent's write can never
turn a stored `false` back to `true`, even from a read made before a
concurrent disable. An agent's change to what an enabled workflow executes
(steps, rollback, trigger, Exec allowlist, actions, workspace, guards,
variables, concurrency limit and key, project) disables it until the user
enables it again, and so does its change to what a Quick API sends or what a
Quick Prompt runs (prompt, variables, agent, connection, tier, skills,
profiles, directives, agent settings), or its name's slug or its project,
and so does an agent's (or an import's, Artifact imports included) creation
of a Quick API or Prompt that shadows a shared `ref:` such a workflow uses,
for every enabled workflow, in any project, that names it by id or holds a
`ref:qa:`/`ref:prompt:` (any field, fan-out or rollback position) with its
old or new slug or any slug it was published or imported under: no reference
is resolved, so the rule is a conservative superset that ambiguity can never
block, and it runs in the same transaction as the write. Quick Exec lines
keep their own KT-1017 rule: an agent's line waits for a human's approval. A
disabled definition never runs: a parent refuses a disabled sub-workflow,
manual and TriggerWorkflow launches, the scheduler and trackers skip it and
only a human can resume an interrupted run of a disabled workflow. Known
residual: a run already in flight, a human-resumed one included, can still
load a Quick API or Prompt an agent changed during that run (rollback and
fan-out included); revision pinning (the approved workflow and its
dependencies frozen at approval) is a 0.15 priority. With localhost trust on
(auth off, or `auth_strict_localhost = false`) a local process without a
bridge token still counts as the user, the trust model already documented
for the HTTP API; strict localhost is the mitigation until positive human
authorization (0.15, KT-1034). The import preview shows the trigger, the
Exec steps and the Exec allowlist, and the agent banners say the workflow is
created disabled (KT-1037).
- Kronn now says why it turned a workflow off on its own. Each automatic
disable (an agent's edit or create, an agent's change to a Quick API or
Prompt it uses, an agent bundle or proposal, an import, a config restore
that fails validation) is recorded on the workflow with its reason, who and
when, and a short summary ("steps changed by Codex", "Quick API « fetch »
edited by Claude"); migration 223. The Automations page shows a banner when
any is waiting ("N workflow(s) were changed by an agent or imported and are
disabled by default"), a details panel with each one's reason, summary,
author, date and link, a Re-enable button per row and a Re-enable all button
(a Cron or Tracker workflow is always confirmed first), and a reason badge
in the workflow list. A human enable clears the record. `GET
/api/workflows/auto-disabled` and `POST /api/workflows/reenable` (one id or
many) refuse a bridge token (KT-1037).
- Outbound requests to a URL a user, a plugin, a provider or an agent supplies
go through one guarded transport (`core::safe_http`): ApiCall (every page,
retry and redirect), OAuth and token exchange, Notify, gate webhooks, media
asset downloads, remote MCP probes, page `web_fetch` and the GitHub tracker.
Its resolver checks every DNS answer and connects only to the checked
addresses, so a rebinding answer cannot reach the socket; IPv4-mapped IPv6
forms such as `[::ffff:127.0.0.1]` are refused like their IPv4 host. Each
redirect hop is re-checked; an https→http downgrade and a cross-origin
307/308 are refused, as is any cross-origin hop that would resend a body (a
301/302 on PUT included); a cross-origin hop drops every header and query
key the plugin, the step or the webhook supplied. IPv6 addresses are
classified against the IANA special-purpose registry (the local-use NAT64
`64:ff9b:1::/48`, `2001:2::/48`, `3fff::/20`, `5f00::/16` and everything
outside `2000::/3` are refused). Peer attachment fetches and remote joins
use the guarded client and never follow a redirect; repository discovery
uses it with same-origin redirects, and an imported GitLab host never
receives the process `GITLAB_TOKEN`: a token only goes to the host it was
configured with (KT-1039).
- A project whose validation discussion has finished no longer stays stuck at
Audited: that discussion archives itself on its last word, which hid the
"Mark audit valid" banner, while the audit timeline's "Validate the audit"
only opened a form to start a whole new validation. The timeline now reads
the latest run's linked validation (same terminal-signal check as the
validate gate), validates the audit directly with an error toast on refusal,
and opens the finished discussion even when archived. A new validation can
only be started while no finished one is linked.
- A Kronn-launched agent's bridge token is checked against what each request
really does: ids nested anywhere in a body or in an imported workflow, task
references, offers, invites and sessions are resolved before the check;
shared resources (project-less or serving several projects) and the agent
library are read but never written; created resources land in the agent's
project and no update can move them out; effects on shared resources run for
the agent's project only; General discussions stay private to their own
launches; every response, whatever the verb, is scoped at any depth with its
counts; the token's project is frozen at first use; every launch gets a token,
dies when cancelled or after 12 hours; the operator token is compared in
constant time (KT-1006). An id field the gate cannot resolve is refused, in
the body and the query; an import hides only the resources it bundles;
every route that reaches a room through a session (workspace, history lease,
link, unlink, transfer, accept-offer, session lookups) checks that room
first, and a token cannot force a session reassignment; a saved Quick Exec
of another project is refused on import and at run time; a workflow export
that bundles another project's resource is refused; refusals no longer name
another project's ids; the bearer scheme is read case-insensitively and a
token never reaches the peer-only routes; a token's media discussion and
learning proposal land in its project. A page or room a workflow step names
through a template must, once rendered, belong to the run's project (a
literal id still works whatever its project), whoever triggered the run;
the keys of a caller's own maps (variables, an external API's path, query
and headers) are no longer read as Kronn ids; the plugin overview and a
page's feeding workflows show a token only its project's; any discussion the
token owns, deleted or moved, ends it; its planning writes are recorded as
an agent's; a plan proposal from a room only touches tasks of that room's
project alone, creates its tasks there and names the project on its card. A
token's workflow may publish only into its own project's pages and rooms; a
workflow run without a project is private to its launch, even after its
workflow moves into a project, and a token's run lists, run filters,
`last_run` and duration estimates (workflow and Quick Prompt) only consider
runs and launches it may see; peer-join cannot
end a session another project's room holds; a config opted into General is
usable by project-less tokens as the plugin overview shows; a page slug can
no longer look like a page id, a page resolves by id first, and a token's
slug conflict names no other project.
- Processes Kronn starts for a caller no longer inherit the backend's
environment: agent CLIs on all three routes, the project and discussion
terminal, workflow Exec steps and workspace hooks, Quick Exec (task
validations included), the API-call credential CLIs and every `git` process
(whose repository hooks run inside it) get a built environment (a reviewed allow-list, then the launch's own
values). No child receives `KRONN_AUTH_TOKEN`, `KRONN_ENCRYPTION_KEK`, or
another agent's provider key, nor a GitHub token from the backend's
environment (only a connected project's, native ACP agents included), and
the backend no longer exports its admin token into its own environment
(KT-1006).
- Native ACP agents (Gemini, Copilot, Kiro, Vibe, OpenCode) now receive their
provider key configured in Kronn, a temporary directory beside the project,
and their room and workflow-step contexts, like the other routes (KT-1013).
They start only with full access (next entry).
- **Known limitation: OpenCode, Vibe, GitHub Copilot, Gemini CLI and Kiro run
only with full access.** These native ACP runtimes load repository plugins,
custom tools, hooks and MCP servers before any permission check, so no
restricted mode can be promised for them. Every launch (discussion, room,
workflow step, Quick Prompt, summary, audit, resume, model discovery)
requires the agent's own setting in Config › Agents › <agent> › Full access,
read from the saved configuration at each launch for the agent actually
started (a workflow reviewer does not inherit its author's access, and
turning the setting off stops the next spawn of a running audit or
workflow); without it Kronn refuses before starting anything, with a
translated message that names the setting, and never turns it on itself. The agent pickers say
"requires full access" beforehand. An audit's own full access no longer
counts for these agents. A per-launch isolated runner is the 0.15 plan.
- Copilot receives Kronn's bridge through `--additional-mcp-config` under a
per-launch name, with `--disable-mcp-server kronn-internal`, so a workspace
`.mcp.json` server named `kronn-internal` is never loaded in its place; a
step's tool list reaches the bridge on every native runtime. A project server
named `kronn-internal*` is dropped, and only the exact bridge id the runtime
registered is trusted.
- The ACP broker no longer takes a tool's identity from `rawInput`, which holds
the model's arguments on every native runtime.
- A bridge-token request with no body labelled `application/json`, as the
bridge sends every GET, was refused with "invalid JSON body": every reading
tool (`disc_meta`…) failed under a bridge token.
- `KRONN_HOST_MCP_SYNC=0` keeps a second Kronn instance (a test or an isolated
check) from rewriting the host CLIs' global MCP configs
(`~/.copilot/mcp-config.json`, `~/.codex/config.toml`, `~/.claude.json`,
`~/.gemini/settings.json`) to its own bridge path.
- The other processes Kronn starts get a built environment too (KT-1006):
dependency checks (`npm`, `cargo`, `go`, `bundle`, `dotnet`, `poetry`,
`composer`, Renovate through `npx`, and Composer through Docker) keep only
their registry, cache and toolchain settings; `gh` and `glab` (PR creation,
PR lookup, `gh auth token`, GitLab discovery) keep their own login and the
connected project's GitHub token, and the git they start inherits nothing
more; `docker compose` keeps only `DOCKER_*`/`COMPOSE_*` settings, so a
compose file can no longer interpolate a backend secret; the `cargo
metadata` run before a validation gets the Quick Exec environment; and the
agent installers, RTK, ccusage, WSL, Tailscale and system probes get the base
allow-list. `glab` no longer reads a `GITLAB_TOKEN` exported to Kronn and a
package manager no longer reads a registry token from Kronn's environment:
both use their own login or config file. Every process now names its
environment when it is built: a spawn without one does not compile, clippy
refuses any other way to start a process (the system opener included), and
the exceptions declared in the design note start in the temporary
directory, never in a repository. The desktop app follows the same rule:
its sleep inhibitor and its login-shell PATH probe no longer pass the
desktop's whole environment on, and its restart relaunches it without the
admin token or the raw key in its environment. Model and version
discovery and the document sidecar now get a built environment too (their
CLI's own login and settings, no credential under any name), and testing
an MCP server, which may come from a repository's
`.mcp.json`, starts it with only its own configured values. An
operator-set `KRONN_ENCRYPTION_KEK` leaves the process environment at
start, like the admin token. On the desktop, only an allow-list (display,
locale, paths, proxies and toolkit settings) stays in the process
environment, so the system webview's helper processes (Linux, Windows)
never inherit a credential, whatever its name; Kronn and its agents still
get every variable, and `KRONN_USE_KEYCHAIN=0` keeps working. Kronn
warns at start when the admin token or the key came from the environment:
the process's original environment block keeps them readable by
same-user processes, so the keychain, the key file or `config.toml` are
safer. npm registry credentials
(`npm_config__auth` and its registry-scoped forms) are dropped like any
other secret.
- `git push` and `gh` PR creation use the project's own GitHub connection,
never the first GitHub MCP token of any project. The token no longer
appears in the push URL or in a variable a repository hook can read: it is
sent as a header scoped to github.com, with hooks and credential helpers
off for that push (KT-1006).
- The project and discussion terminal no longer runs `env` and no longer
reads outside the project: `cat`, `head`, `tail`, `find`, `stat`, `grep`,
`rg`, `wc`, `du`, `file`, `tree` and `ls` refuse a path that resolves
outside it, symlinks followed, and `find -exec`/`-delete` and
`git diff --no-index` are refused (KT-1006).
- The encryption key can no longer be lost to an unreadable vault or to
encrypted data the boot did not look at (KT-1007). A denied or locked
keychain, or an unreadable `encryption_key` file, now stops Kronn at startup
with a message saying what to do, instead of being read as empty and
overwritten; a key is generated only when no encrypted column holds data
(MCP settings, execution-variable snapshots and stored credentials, all
listed in one registry checked against the database schema). When no key
decrypts existing data, Kronn keeps running for a restore but holds no key,
so nothing new is encrypted under a key that no vault keeps. Two keys that
each decrypt part of the data stop the startup with nothing written, and a
vault holding another key is never overwritten; starting once with
`KRONN_REENCRYPT_FROM=<fingerprint>` moves one key's data under the other
without deleting either. A stored API token that can no longer be decrypted
locks the API (only health and the recovery routes answer, the interface
shows a "Kronn is locked" restore screen, and an exposed install still
starts) instead of leaving it open; a credential start-up that fails locks
it too. Invalid key values are never adopted, and a missing `config.toml`
no longer offers a random key that could be kept for good.
- Replacing the recovery passphrase now asks for the current one, and
`recovery.key` records which key it wraps, so a blob for another key never
counts as a recovery copy (KT-1007).
- Restoring a recovery passphrase on a running instance no longer swaps its
key (which split secrets between two keys): an import keeps the source
machine's recovery data beside this machine's, and Plugins → "Re-encrypt
imported secrets" re-encrypts the imported secrets under this machine's key
(KT-1007).
- Reset keeps a copy of the key that `config.toml` still has to hold, keeps
the API token so a network-exposed instance stays authenticated, clears
execution-variable snapshots, provider keys and GitHub tokens, and reports
a failure instead of answering success (KT-1007).
- Re-importing this machine's own backup no longer deletes the stored GitHub
tokens of its projects, and an import runs in one transaction: a failing
project or MCP insert leaves the previous data in place (KT-1007).
- While the encryption key is locked, no provider key, connection key or API
token is ever written to `config.toml` in clear: such changes are refused,
key auto-discovery waits, and an operator's `KRONN_AUTH_TOKEN` serves the
session only (KT-1007).
- A key kept only in a `config.toml` backup or a set-aside file is now found
when it still decrypts some rows, instead of leaving them unreadable; a
recovery passphrase that protects another key can be replaced without it;
re-encrypted provider keys appear without a restart; an export of secrets
without a recovery passphrase warns; a `config.toml` set aside at start is
shown in the setup wizard; every backup copy is scrubbed even when one
fails; and key files are synced to disk with their directory (KT-1007).
- Secrets under an older key kept only in a `config.toml` backup, a set-aside
value or a corrupt `config.toml` are re-encrypted under the key in use at
start (the file is kept) instead of stopping the start; a set-aside
`config.toml` and a credential load failure are shown app-wide; a reset
while the key is locked sets up a new key at once; a provider key whose
save fails no longer appears saved; and a recovery passphrase from before
0.14.3, or a damaged one, can be replaced from Settings (KT-1007).
- A lost key with no recovery passphrase no longer holds the rest hostage:
"set the locked secrets aside and start a new key" keeps them in a file and
keeps discussions, projects and workflows. A failed reset while locked keeps
the API authenticated; moves from kept key files wait for a durable copy and
never stop the start; connection keys, LiteLLM and discovered keys appear
only once saved; credentials of an unreadable `config.toml` are recovered;
a forgotten passphrase can be replaced; and the desktop restart keeps the
operator's API token (KT-1007).
- Secrets set aside when a key was given up come back through "Re-encrypt"
once the old passphrase or code is offered; starting a new key or a locked
reset never turns API auth off (or on), keeps a readable token, stops on an
unreadable key store before writing anything, and removes only rows that are
still the ones copied (KT-1007).
- A credential store that cannot be written after starting a new key or a
locked reset no longer leaves the API open; secrets put back from a
locked-secrets file survive a failed reload; the restore reports what it put
back and what still waits; and an export warns that locked-secrets files stay
on this machine (KT-1007).
- The same key written in upper and lower case is one key (it used to stop
the start-up as "several keys"), a damaged key value no longer crashes the
start-up, and an older `config.toml.backup` is kept under a timestamped
name instead of being overwritten (KT-1007).
- A first start that cannot keep a durable copy of the new key (disk full,
read-only data directory) stops with what to do instead of encrypting
under a key that would be gone at the next start; an invalid or retired
`encryption_secret` in `config.toml` is set aside in a side file and never
keeps the key in use from being saved; a reset while the key is locked
lets the next start begin fresh; `config.toml.backup` copies rotated from
0.14.2 lose their plaintext credentials; an unreadable `config.toml` is
kept aside and its key salvaged (KT-1007).
- The locked screen tells a remote visitor to open Kronn on its machine,
restores a lost key, or sets a new API token when only the stored token is
unreadable (even with strict localhost); Settings → Recovery restores a
locked key and warns when `recovery.key` protects another key or a key
store holds another key, and a banner appears when secrets are locked
(KT-1007).
- Kronn on Windows starting Claude or Codex installed in WSL now passes them
what they need across the boundary: every variable it sets for the launch is
listed in `WSLENV` (appended to the user's own list), and the Windows paths
it puts in the arguments, including the bridge command inside the inline MCP
configuration, the sandbox settings and Codex overrides, become WSL paths.
`KRONN_WSL_BACKEND_URL` gives a WSL agent a backend URL it can reach in
WSL2's NAT mode, and a warning says so when networking is not mirrored. A
Gemini, Copilot, Kiro, OpenCode or Vibe found only inside WSL is refused
with a clear message instead of failing to start (KT-981).
- An audit step that only calls tools, without a word of text, now shows the
category of its last action, its call count and its tokens while it works, for HTTP agents and
ACP agents alike, through one probe; the counters no longer wait for a text
line. A silent audit stream sends keep-alive comments every 15 seconds,
which hold the connection without passing for model activity (KT-950).
- Importing a plugin bundle now shows, per plugin whose bundled command line
differs from the catalogue's, the usual and the proposed command (secret-looking
arguments masked) with an unchecked "I accept these arguments" box. Consent is
per plugin, and re-importing an already imported bundle can add it (KT-1010).
- An old single-plugin JSON from the removed per-plugin export dropped into the
bundle import is recognised and imported as a one-plugin bundle, with the same
review; a malformed one gets a clear error (KT-833).
- Large databases stay responsive (KT-1019). The token statistics and the
workflow list no longer read every run's step results to reach a few
columns: a covering index answers them, on the read connection. Scheduled
and manual backups copy a read snapshot with `VACUUM INTO` instead of
holding the write connection, so runs and discussions keep writing during
the copy, and they are refused with a clear log when the disk lacks the
space. The backup taken before a migration is written to a temporary file
and renamed, so a full disk no longer replaces the previous good backup,
and a failed copy stops the upgrade instead of migrating without a backup
(`KRONN_MIGRATION_BACKUP=0` upgrades without one, on purpose).
- The desktop release pipeline now fails before building when the pushed tag
differs from `VERSION` or a version marker is stale, builds the release job
from the requested tag on a manual run, and expects exactly the installers
Tauri is configured to produce (`.exe`, `.dmg`, `.deb`) instead of accepting
an `.msi` or `.AppImage` in their place; the release notes give the real
`.deb` file name. The release commit is resolved once and the checkouts,
the dependency review and the full CI gate (label-gated jobs included) run
on it, and the release waits for that gate (KT-1023).
- Opening a project shows the last known Git and Dependencies results at once,
with their date, and refreshes them behind a discreet indicator. A failed
refresh keeps the previous result instead of blanking the block, and the
Git block renders local status before the pull-request lookup completes
(KT-989).
- Plugins page: the toolbar button that rescans the projects' `.mcp.json` files
is labelled as such instead of "Sync", the preview names how many projects
are affected, and the report after applying lists created, merged, rewritten
and removed counts (KT-833).
- Turning on full access for GitHub Copilot CLI failed with "Agent does not
support access flags". The backend now accepts every CLI agent the interface
offers, and each one describes what it really widens (KT-975).
- A guarded action (save, create, toggle) now always runs with the current
props and translations instead of the ones from the first render, and a
browser with disabled or full storage no longer crashes the Discussions,
Dashboard, Workflows or Settings pages when they remember a preference
(KT-1022).
- Listing projects no longer writes `docs/.kronn.json` into a checkout that
only carries legacy audit markers (KT-993): the status is computed in memory
and the file is written by audits and validations only, atomically.
- A validation no longer asks again about a TD already decided (KT-938): the
decision a card writes on the TD sheet (confirmed, rejected, accepted
decision) is read back, so the full validation after a resume asks only
about new or undecided TDs and names the partial validation it follows. A
deferred TD is asked again, since its decision was postponed. Re-audits stop
carrying rejected TDs and accepted trade-offs in the index, and no longer
report them as missed. A TD whose status sits only in its YAML front matter
is read and updated too.
- An audit of an already documented repository no longer fails on dead links
in documents Kronn does not own (KT-1020): moved `docs/legacy/` documents and
root instruction files with human content outside Kronn's block are
reported, not blocking, and the repair step never rewrites them.
- Audits no longer receive the `kronn-internal` and Memory MCP servers
(KT-935): the `.mcp.json` filter and the ACP broker apply the same exclusion,
so an OpenCode, Claude or Codex audit gets neither Kronn's write tools nor a
knowledge graph shared across projects, and the refusal is logged instead of
appearing as a registry error. Discussions on the audited project keep
their tools.
- The documentation template and audit are more consistent (KT-934): a
validation is refused while a document Kronn owns still carries a
placeholder or a `TODO:` marker, the final review skips the `TEMPLATE.md`
gabarits like the document gate does, and no root redirector is written
toward a missing `docs/AGENTS.md`.
The 800-word budget of the documentation skeleton now counts raw words, HTML
comments included, because that is what the agent reads; the skeleton went
from 892 raw words to under 800 by dropping comments that repeated visible
guidance (KT-934).
- A resumed audit keeps the tokens, duration and cost of the steps it inherits
and names the run that spent them, refuses to start when it cannot record
them, and warns when the sources moved since the run it continues (KT-1021):
each audit run now records its commit, branch and source fingerprint. The
timeline shows a step whose agent reported nothing as unknown instead of
hiding it.
- An audit step's token figure is now its fresh traffic, uncached input plus
output, for the agents whose cache accounting Kronn's pricing knows (Claude,
Codex); for the others it stays input plus output as reported, and the hover
says the cache split is unknown. Hovering lists uncached input, output,
cache read, cache write and the total with cache. The live counter, the
run's total and the audit recap use the same figure; rows recorded before
this keep the figure they were stored with, without a breakdown. A step whose
agent reported no cost gets an estimate from Kronn's rate table for the model
its run served (shown "≈"), only where that split is known; otherwise the
reason is shown in the user's language. One estimated step makes the run's
total estimated.
- Natively, a Claude discussion kept none of its project's MCP servers once
one of them carried a credential in its environment (KT-1003): the whole
`.mcp.json` was refused. Each authorized server now stays on its own. Its
environment values reach Claude as `${KRONN_MCP_…}` references in the
command line and as values in the Claude process's own environment, never in
argv, ACP payloads or events. A credential passed in `args` still leaves its
server out.
- Security hardening (KT-1009). The WebSocket no longer streams every live
event to any private-network peer: without the API token only loopback (or,
under Docker, the gateway's local clients) counts as the local UI, and
`X-Real-IP` is honoured only from the Docker gateway (`X-Forwarded-For`
never). The container no longer writes the GitHub token into `.netrc` for
gitlab.com (a `GITLAB_TOKEN` feeds that entry, and a stale one is removed).
Saving an OpenAI key rewrites `~/.codex/auth.json` atomically, owner-only,
and keeps a ChatGPT login's mode. Key and recovery temp files are created
owner-only and never follow a planted symlink, scheduled backups are created
0600, masked values no longer panic on accented or emoji edges, the agent
read guard also refuses Kronn's key, config, database, `.mcp.json` and MCP
backups, and exports drop the failure webhook URL.
- A plugin bundle can no longer change the command a trusted plugin runs
(KT-1010): custom arguments in a bundle replace the plugin's whole command
line, so an import now drops them, and says so, unless the importer gives
explicit consent (`accept_args_override`). Custom arguments, which may hold
tokens, now travel only in the encrypted bundle, and a manual import can no
longer claim a `mcp-` or `api-` id reserved for the built-in catalogue.
- Native skill and agent files are safer to write (KT-1011): nothing is
written through a symlinked folder such as a `.claude/skills` pointing
outside the repository, every front-matter value is quoted so a newline in
a skill description can no longer add a key like `allowed-tools`,
concurrent runs on one project no longer drop each other's entries from the
ownership ledger, and "Migrate to .agents/skills" no longer offers the
catalogue copies Kronn synced itself as repository skills.
- The desktop app keeps its local port from one launch to the next, so the
interface settings stored by the browser no longer reset at every launch or
after "Allow connections from other devices" restarts it (KT-972). The port
is saved in `desktop-port.json` in the data directory and can be set from
Settings → Identity ("Desktop port", 1024 to 65535, desktop app only, with
a "Restart now" button) or pinned there by hand; a busy port is replaced for
that launch only. The port is held from
the moment it is chosen, and the app only opens a backend that proves it is
the one it just started, never another Kronn answering on the same port.
Interface preferences (theme, tour progress, folds and sidebars, favourites,
default project, dismissed update) are also kept by the backend and
restored on a new origin; drafts and unsent messages stay on the device.
- A task delegated to Gemini, Copilot, Kiro or OpenCode as a launched worker
ran with the discussion's full access and without its delivery context, so
it could act beyond the worker scope and never deliver (KT-1012). These
native ACP agents, and Vibe, are now refused as launched workers with a
clear reason,
at preparation and at launch; one worker policy now decides every route, and
an exact joined CLI session of the same agent stays eligible.
- Agent streams are sturdier (KT-1014). An accented letter or emoji split
across two network chunks of an HTTP model's reply (Ollama, LiteLLM,
OpenRouter, NVIDIA, Custom) is decoded intact instead of becoming two `�`,
which could also corrupt a file path in a tool call. A non-UTF-8 byte on a
CLI agent's output costs one replacement character instead of stopping the
reader and leaving the agent blocked until the watchdog; a native ACP agent
keeps its session. After a Claude or Codex turn exits normally, the
processes it left behind (stdio MCP servers, background commands) are
stopped with it instead of piling up.
- When Ollama refused an unreadable tool call and Kronn replayed the request,
the text the refused attempt had already streamed (a preamble before the
call) appeared twice in the reply and in the saved message (KT-944). On an
Ollama turn that offers tools, the first 512 bytes of an attempt are now held
until the attempt ends: a replayed attempt's preamble is dropped, a
successful one is shown, and a longer answer streams live as before.
- An audit step on a hosted large-context model (OpenRouter, LiteLLM, NVIDIA,
Custom) could explore for hours, resending its whole history each round,
because context pressure was never measured on that wire and the write
window only opened at round 242 of 250 (KT-998). Pressure is now measured on
a bounded step budget (the smaller of the model's window and 128 000
tokens), for audits and HTTP workers alike, and an audit step writes its
deliverable once it has spent 1 500 000 input tokens. Both budgets can be
set with `KRONN_HTTP_STEP_CTX_BUDGET` and
`KRONN_HTTP_AUDIT_STEP_INPUT_BUDGET`.
- The cost an agent reports itself is no longer lost (KT-997, backend part).
Claude Code names it `total_cost_usd`, which Kronn did not read, and the
default Claude route dropped it anyway; OpenRouter's `usage.cost` was never
read. Both now reach the run's usage as integer micro-USD, summed per
response, and stay unknown (never zero) when not reported. The audit
timeline shows it per step.
- A workflow run now keeps the project it was launched in (KT-1015). A global
workflow launched from a project lost that project after a gate approval or a
resume and continued with no working directory, and its worktree was never
cleaned at startup. A sub-workflow pinned to another project now runs in that
project's repository instead of the parent's worktree; as a foreach child it
is refused, since it would have to share the parent's worktree.
- One failing workflow no longer cancels the scheduler's tick (KT-1016): a
missing variable or an unreachable tracker stopped the loop, and every cron
occurrence of the workflows after it was lost for good. A tracker issue is
now marked processed only once its run is admitted, so an issue refused by
the concurrency limit or the preflight is polled again instead of never
running.
- Workflow run lifecycle holes (KT-1018). Approving a gate whose worktree had
disappeared continued the run in the main checkout; it now fails with the
reason instead. A run paused on a gate keeps its concurrency slot, and a gate
approval or an interrupted resume is refused while the limit is reached. A
run that stops on an error removes its worktree right away (keeping a branch
that holds unintegrated commits) instead of waiting for the next start, and
a failing or hung `before_remove` hook no longer keeps the worktree. Adding a
Gate to a workflow another one uses as a sub-workflow is refused, and a
workflow imported from a repository's `kronn/` folder goes through the
editor's save rules. Isolated workflow worktrees get the project's agent
configs (`.mcp.json` and the others), so an Agent step keeps the project's
MCP servers and its strict MCP config instead of the host's.
- Workflow worktrees no longer pile up as `prunable` entries (KT-985). A
worktree a step created inside its run's worktree (`.kronn/pr-N`) is now
removed with it, and at startup Kronn drops the stale entries of its own
worktrees under `.kronn/` in every project, leaving the user's other
worktrees alone. A run still keeps a branch that holds commits no base has,
whether it succeeded, failed or was cancelled.
- A tracker issue title or a step output can no longer run as code in an
Exec step (KT-1017). Saving a workflow now refuses any template value in an
interpreter's inline code (`bash -c`, `python3 -c`, `node -e`…, attached
forms like `-cCODE` or `--eval=CODE` included), except `{{run.id}}` and
`{{time.now…}}`, and says how to write it safely: pass the value as a later
argument the interpreter never parses
(`["-c", "echo \"$1\"", "_", "{{issue.title}}"]`). A new `{{value|sh}}`
filter renders one single-quoted shell word, but it is a quoting helper, not
a way into inline code. Quick Exec applies the same rule when it is saved
(an unchanged stored line stays editable, and still does not run), and the
Quick Exec form and the workflow editor now show such a refusal in full,
with the suggested arguments, instead of a generic error.
A value may also no longer sit where an interpreter still reads its
options (`["{{mode}}", "{{issue.title}}"]` with `{{mode}}` rendering to
`-c`): it is accepted only after the inline code of a shell or Python,
after `--`, or after a script file, and the run-time check verifies the
rendered command with the origin of each argument. The same rule now
sees through programs that launch others (`env`, `sudo`, `timeout`,
`nice`, `xargs`, `busybox`…) and covers other evaluators (`awk`, `sed`,
`osascript`, `lua`, `tclsh`, `Rscript`, `find -exec`, `git -c`, `ssh`,
`docker run`, `npx -c`), whatever path or version suffix names them
(`/usr/bin/python3.12`, `node18`, `.exe`); a value can never be the program
itself; and the workflow bundle, the workflow and Artifact imports, the
Quick Exec import and the `kronn/` import apply it too. An unsafe line is
kept on save only when it is exactly the stored one (same step, position,
name and arguments), and an unquoted shell value no longer gets an
automatic fix, since `"$1"` would not print the same thing. It also knows
that a shell never glues its script to `-c` (`bash -cx SCRIPT`), covers
`make --eval` and `VAR=value`, `npm`/`pnpm`/`yarn exec`/`dlx`/`install`/`run`
and `bunx`, `gdb -ex`, `tar --to-command`, `rsync -e`, `curl -K`, `vim -c`
and `+cmd`, `sqlite3`, `mysql -e`, `psql -c`, `crontab` and `at`, refuses
any value given to an interpreter it does not model, and refuses an
`exec_stdin` value when the program runs its stdin as code (a shell or an
interpreter without a script or inline code). A config restore imports a
workflow that fails these rules disabled and leaves such a Quick Exec out,
and its report names each one and why. Option clusters of launchers
(`sudo -Eu`, `env -iS`), `docker compose`, git options and subcommands
that run commands (`rebase --exec`, `submodule foreach`, `bisect run`,
`git config` values), more launchers (`watch`, `flock`, `kubectl exec`,
`chroot`, `strace`, `parallel`…) and package fetchers (`pip install`,
`pipx run`, `uvx`, `go run`, `cargo install`, `gem install`,
`deno run`/`eval`) follow the same rule.
- A run value can no longer reach a program Kronn does not model unless a
human approved it (KT-1017). Every program is now either known to read
its arguments as plain data (`echo`, `printf`, `date`…), parsed by the
classifier, or refused: an Exec step or Quick Exec that passes a template
value to any other program is refused at save and at run time, with the
program named. The step editor and the Quick Exec form show a checkbox for
such a line ("terraform receives values from the run; I confirm it treats
its arguments as plain data"); only a human can tick it, an agent's save
never sets it, and a bundle, an Artifact import or an import through an
agent drops it.
- A run value given to a data-only program that still reads options (`date`,
`seq`, `basename`…) is refused at run time when it renders to an option
(`date {{x}}` as `-s…`), unless a literal `--` precedes it (KT-1017).
- A template value glued to an option (`-{{x}}`, `--{{x}}`, `-v{{x}}`) is
refused unless the option before it is one the program reads a value from
(`mysql -u{{user}}`, `git commit -m{{msg}}`, `--name={{value}}`) (KT-1017).
`sqlite3 --cmd`/`--init`, the command after BSD `script FILE`, and jq's
`-f`/`-L` files are code like `-cmd`, `script -c` and an inline program.
- An `exec_stdin` value reaching a program Kronn does not model is refused
unless a human approved the step, as for its arguments (KT-1017).
- `run.*`, `time.*` and `now*` are reserved: a workflow, launch, trigger or
CollectApiData variable of that name is refused at save and at trigger, and
`{{run.id}}` / `{{time.now}}` always resolve to Kronn's own values
(KT-1017). An approved CollectApiData source is no longer refused at run
time.
- Kronn now trusts a run value without a human only in five exact shapes,
`bash|sh -c SCRIPT NAME ARGS…`, `python3 -c CODE ARGS…`,
`python3 SCRIPT ARGS…`, `node -e CODE -- ARGS…` and `node SCRIPT ARGS…`,
with no other option and no value in the code or script, and in data-only
programs (KT-1017). Every other line, `git`, `make`, `docker`, `npx`,
`bash -ec` and wrappers included, needs a human's approval; code, option
and program positions stay refused, approved or not. A templated
`exec_stdin` is trusted only by data-only programs and those shapes; a
shell reading its program from stdin, `/dev/stdin`-style scripts, `-f -`
and `python -i` count as code.
An approved Quick Exec now runs from its page, and "Suggest a fix" moves
shell flags such as `-e` into a `set` line.
- An Exec line or Quick Exec an agent writes (bridge token or Kronn's agent
tools) needs a human's approval for any run value, whatever its shape
(KT-1017): the server records who last wrote each line, and an agent can
neither approve nor launder a value through a `bash -c` script. Such a line
is saved and the run refuses it until a human approves it; the save result
names the waiting steps (`notice`, `kronn_notice` for agents), and the
workflow page and step editor show "written by an agent: needs your
approval" with the line and an approve action. Inline CollectApiData
sources get the approval box in the editor.
- Whatever originates from an agent is agent-written, whatever the transport
that carries a bridge token or an agent signal (KT-1017; residual for 0.15:
a local agent that drops its bridge token is still trusted as a human through
loopback): a `KRONN:WORKFLOW_READY` or `BUNDLE_READY` proposal accepted by a
click, and a new or changed line of a `kronn/` file, wait for an approval in
the editor; the creation banner and the approve sheet list the Exec lines.
The writer is tracked per line (main, setup, stdin, each source) and a line's
identity includes its step type and pinned scripts; fields a step type never
reads are dropped on save. An agent never supplies a script hash, and an
empty hash is pinned only by a human's save of that step. A human can approve
an agent-written Quick Exec (writer and approval are stored apart). A config
restore keeps approvals only from a backup this instance sealed. Launchers
that start a default shell (`su`, `script`, `chroot`, `unshare`, `nsenter`,
`sudo -s`, `newgrp`, `sg`) and stdin paths climbing to `/dev` count as code
readers, and a script fed by a templated stdin must be a regular file at run
time. `date` options are matched in clusters and abbreviations. Option
values never hide a default shell (`sudo -u u -s`, `script -t 0 FILE`), an
`awk|sed|make|psql -f` or `lua`/`Rscript` program file fed by a templated
stdin must be a regular file, script hashes carried by an agent-origin import
or an unsealed restore are blanked, the stdin line has its own writer, and
the restore seal is compared in constant time. A script
shape (`python3 tool.py …`) needs its script pinned in `exec_script_files`,
checked at run time to be a regular file; a shell script given to `-c`
never starts with `-` or `+`; shells are not trusted shapes on Windows.
Only `echo`, `printf` (literal format), `test`, `true`, `false`,
`basename`, `dirname`, `seq` and `date` stay data-only: programs reading or
writing paths (`cat`, `grep`, `jq`, `cp`, `tee`, `rm`…) or the environment
(`printenv`) need the approval. Every import drops an approval except a
restore of this instance's own backup; the `kronn/` import keeps only one
this instance gave to the very same line. One approval covers the whole
step: the editor lists every line it covers and an approval no line needs
is dropped on save. Agent background jobs run a saved Quick Exec with the
same checks, and a code reader behind `npx`, `kubectl exec -i`,
`docker compose run`, `runuser`, `xargs` or `parallel` stays refused when
approved.
- Security: dependency advisories. Frontend overrides now pin `sharp` 0.35.5
(GHSA-wq5f-xc86-pv6w, librsvg), `source-map-js` 1.2.2 (GHSA-68fv-2mgg-jv7q,
denial of service), `dompurify` 3.4.16 (GHSA-p98j-92pf-mc4p,
GHSA-6688-9rhm-gjv2) and `global-agent` 4.1.3, which drops the `sprintf-js`
chain behind GHSA-hp3w-g68c-fv3c (no patched `sprintf-js` exists). Only
`onnxruntime-node`'s install script uses `global-agent`.
### Changed
- Settings: in every agent card (agents, Ollama, external APIs) each tier's model picker now spans the card's full width, one tier per line, and its list opens taller. Model names share long provider prefixes such as "OpenCode Zen/…" and were unreadable in the narrow control.
- Agents no longer receive the machine's GitHub token by default (KT-1006, D2):
`GH_TOKEN`, `GITHUB_TOKEN` and `COPILOT_GITHUB_TOKEN` reach Claude and Codex
launches only for a connected project, and are removed from the inherited
environment otherwise, Docker included. On upgrade, existing projects whose
remote is on GitHub stay connected through the gh login and show a one-time
notice with a "Turn off" button; new projects start not connected. Workflow
Exec steps follow the same project setting, and a failed `gh` step says the
project is not connected. `gh auth token` now runs asynchronously, with a
5-second limit and a one-minute cache, instead of blocking every launch.
- A request whose bearer token matches neither the operator token nor a live
bridge token is refused, even from loopback, where a request without any
token keeps passing (KT-1006).
- The project and discussion terminal runs its command without a shell:
quotes still group words, but `$VAR`, `~` and globs are passed literally
(`find . -name "*.rs"` still works) (KT-1006).
- `config.toml` no longer holds secrets (KT-1007): provider and External API
connection keys and the API auth token are stored encrypted in the database,
and the encryption key stays in the OS keychain or the `encryption_key` file,
kept in `config.toml` until two independent copies exist (two key stores, or
one plus a recovery passphrase for that key); a sidecar-only install
without a passphrase keeps it there. The first start moves
existing values, checks each one decrypts back, keeps the previous file
encrypted as `config.toml.pre-credential-store.enc`, and can be interrupted
and rerun without loss. The `config.toml.backup` copy made before database
migrations is written owner-only and without credentials. Downgrading to
0.14.2 hides the stored keys until the next upgrade. See
`docs/operations/key-management.md`.
- Plugins page: one export and one import flow, the plugin bundle, where each
plugin's scope and CLI exposure are chosen on import. The per-plugin JSON
export and the paste-a-spec import are gone, and a plugin is deleted from
its detail sheet only, behind its two-step confirmation (KT-833).
- The release notes are now generated from the installers a release really
carries (KT-1004): one direct download link per attached file, the version's
CHANGELOG section above the install table, then the Linux and macOS install
notes. A missing platform or CHANGELOG section fails the
release job instead of publishing a draft that names absent files.
- A workflow launch card, in a discussion or on a Live Page, shows only the
values you can fill as fields. The values Kronn resolves itself go into one
folded line, "N values resolved at launch", closed by default, that lists
each one's name, description and origin, without greyed-out fields
(KT-1024).
## [0.14.2] - 2026-10-03
### Added
- The project card's Audit tab is now a timeline (KT-977): briefing,
template, the audit's steps grouped as the pipeline runs them (documentation
core, specialised audits, consolidation), validation, validated. Every step
shows, before it runs, the file it writes and what it covers (from the new
read-only `GET /api/audit/steps`, built from the chain the pipeline runs),
then its status, a readable reason when it failed or was interrupted, its
duration and when it last ran. A resumed run is shown whole: its steps are
merged with the run it continued. A failed group resumes from its own button,
and the timeline says that the consolidation reruns after it. The agent panel
groups installed CLIs (recommended), HTTP agents and local models, shows the
model of each level (⚡ Eco, 🎯 Standard, 🧠 Advanced, the recommended one)
and warns about lower levels, local slowness and data sent to an HTTP
provider. The briefing is filled in and saved from the timeline without
opening a discussion, and a note explains what changed in the audit.
- Any HTTP agent can now run an audit, at the user's choice (KT-980): NVIDIA
and named connections such as OpenRouter join Ollama and LiteLLM. Full and
partial audit requests take an optional `connection_id`; the audit uses that
connection's endpoint, key and model for the chosen level, and its
validation discussion keeps the connection. A `Custom` agent without a
connection is refused by name. Vibe, which has no file tools, still cannot
audit.
- The Automation page lists the skills. "Skills" is a fifth type in the
sidebar Filter, with its count, next to Workflows, Quick APIs, Quick Prompts
and Quick Execs, and covers the built-in skills as well as the ones you
wrote. A skill sits under every project that lists it among its default
skills, and under "No project" when none does; the flat lists (Favorites,
Recent) show it once. Opening one shows its sheet in the main column: name,
description, category, the projects that use it, and its `SKILL.md` — rendered
as Markdown without raw HTML (the same rendering as a repository resource) or
as Source. Name and description are searchable, like the other types. The
sheet is for reading: no variable, no launch. A skill is still edited in
Config › Skills, which the sheet links to; a skill you wrote can be deleted
from its row or its sheet, a built-in one cannot. A skill has no pin on the
server, so its favorites are kept in this browser. Merging skills with Quick
Prompts stays KT-906 (KT-914).
- Skills are real Agent Skills, written where agents look for them. Writing a
skill into the repository now produces `.agents/skills/<slug>/SKILL.md` in the
standard format (valid `name` and `description`, Kronn's own fields under
`metadata`), never `kronn/skills/`, which keeps only what has no native home.
A repository that already has `