Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
da8dbdc
I've implemented #3542: the plugin manifest schema in `@propr/shared`…
propr-dev[bot] Nov 14, 2026
7691cf4
feat(ai): I fixed the CodeQL warning in `scripts/check-plugins.mjs:13…
Nov 14, 2026
d683491
I've implemented #3543. An administrator can now install a plugin and…
propr-dev[bot] Nov 14, 2026
4e4052c
I've implemented #3544. An enabled plugin now adds its MCP servers an…
propr-dev[bot] Nov 14, 2026
69abdd1
ci: pin hosted Linux jobs to ubuntu-24.04
proprdev Nov 14, 2026
69b8e53
Merge pull request #3569 from integry/ci/pin-ubuntu-24-04-plugins-epic
integry Nov 14, 2026
19acf4c
I've built the Plugins page for #3545. In a browser test, the example…
propr-dev[bot] Nov 14, 2026
785750b
The Plugins page is finished: settings are now edited through a form …
propr-dev[bot] Nov 14, 2026
d9fe03c
The repository event journal now has a `task` domain. Every task type…
propr-dev[bot] Nov 14, 2026
c7aad41
Nothing changes here either: the second copy of that wait loop also h…
Nov 14, 2026
85d63e8
feat(ai): I've fixed all four findings and added regression tests for…
Nov 14, 2026
f107020
merge: resolve conflicts from main into 3542-epic-plugin-system-xu8
Nov 14, 2026
d30665f
feat(ai): I fixed all three findings (F5, F6, F7), each with a regres…
Nov 14, 2026
8c53262
feat(ai): I fixed the CodeQL alert. The plugin store no longer hashes…
Nov 14, 2026
20a2652
feat(ai): The alert is already gone from the current code. CodeQL fai…
Nov 14, 2026
3c22120
feat(ai): I fixed F8 and F9 and added regression tests for both. With…
Nov 14, 2026
ca5eb57
feat(ai): I fixed F10 and F11 and added regression tests for both. Wi…
Nov 14, 2026
9adc1ac
feat(ai): I fixed F12: `ctx.propr` now writes an attributed `handler_…
Nov 14, 2026
e641ac9
feat(ai): I fixed both findings, F13 and F14, and added a regression …
Nov 14, 2026
d7e2ecf
feat(ai): I fixed F15. Credentials echoed with percent escapes, in ei…
Nov 14, 2026
4222b03
feat(ai): I fixed F16. The handler runner now re-redacts every captur…
Nov 14, 2026
1051eeb
feat(ai): Both findings are fixed, with regression tests that fail on…
Nov 15, 2026
6a5a4a5
feat(ai): F19 is fixed. `ctx.propr.getTask` now authorizes the task's…
Nov 15, 2026
ab0ed1a
feat(ai): I fixed F20. Log messages and handler error messages are no…
Nov 15, 2026
4660202
feat(ai): I fixed the Shard 2/4 failure. It was a timing flake in a t…
Nov 15, 2026
35afbab
feat(ai): I fixed the CI failure. The finalize step itself was fine: …
Nov 15, 2026
c259678
feat(ai): I fixed F21: `runPluginHandler` now returns a failed outcom…
Nov 15, 2026
0f5ff6b
Merge pull request #3573 from integry/3548/claude-opus-5-5-plugin-han…
integry Nov 15, 2026
82c887d
Plugins now receive the repository events they subscribe to. A daemon…
propr-dev[bot] Nov 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .github/workflows/pr-build-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1028,6 +1028,23 @@ jobs:

fi

# --- PLUGINS ---
# Official plugin manifests (packages/plugins) are validated against
# the @propr/shared schema on every pull request, like the automation
# template catalog: the check reads TypeScript sources through tsx
# and needs no build. `--online` is for local use only.
echo -e "\n--- Plugins: Manifest Check ---" >> build_log.txt
PLUGINS_FAILED=0
PLUGINS_OUTPUT=$(npm run plugins:check 2>&1) || {
echo "❌ Plugin manifest check FAILED (run npm run plugins:check)" >> build_log.txt
echo "$PLUGINS_OUTPUT" >> build_log.txt
PLUGINS_FAILED=1
EXIT_CODE=1
}
if [ $PLUGINS_FAILED -eq 0 ]; then
echo "✅ Plugin manifests are valid" >> build_log.txt
fi

# --- API REFERENCE ---
# The OpenAPI spec is generated from the API route registry, published
# by the docs site and consumed by @propr/client, so a change to any
Expand Down
22 changes: 17 additions & 5 deletions apps/desktop/scripts/packaged-acceptance-csp-reload.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -562,15 +562,27 @@ test('exact observer preserves native denied evidence across real Chromium navig
response.end(`<meta http-equiv="Content-Security-Policy" content="${policy}"><title>CSP fixture</title>`);
});
const listen = server => new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
const close = server => new Promise((resolve, reject) => server.close(error => error ? reject(error) : resolve()));
const close = server => new Promise((resolve, reject) => {
server.close(error => error ? reject(error) : resolve());
server.closeAllConnections();
});
// Teardown is bounded: an unbounded browser.close() in an after hook once
// held this CI job for ten minutes with only the main Chromium process left,
// which skipped the whole unsigned packaging matrix. The browser runs as an
// owned server process so a stuck close can be escalated to SIGKILL.
const within = (promise, ms) => Promise.race([promise.then(() => true, () => true),
new Promise(resolve => setTimeout(resolve, ms, false).unref())]);
let browserServer;
t.after(async () => {
if (browserServer && !await within(browserServer.close(), 10_000)) browserServer.process().kill('SIGKILL');
await Promise.all([api, document].filter(server => server.listening).map(close));
});
await listen(api);
t.after(() => close(api));
const fixtureOrigin = `http://127.0.0.1:${api.address().port}`;
await listen(document);
t.after(() => close(document));
const documentOrigin = `http://127.0.0.1:${document.address().port}`;
const browser = await chromium.launch({ args: ['--no-sandbox'] });
t.after(() => browser.close());
browserServer = await chromium.launchServer({ args: ['--no-sandbox'] });
const browser = await chromium.connect(browserServer.wsEndpoint());
t.diagnostic(`Chromium ${browser.version()}`);
const page = await browser.newPage();
await page.goto(documentOrigin + '/denied');
Expand Down
435 changes: 435 additions & 0 deletions docs/docs/architecture/plugin-runtime.md

Large diffs are not rendered by default.

48 changes: 42 additions & 6 deletions docs/docs/architecture/repository-event-journal.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
---
sidebar_label: "Repository Event Journal"
title: "Repository change-event journal"
description: "The durable, append-only journal of pull request, epic queue and plan issue changes: table, event types, payloads, idempotency and retention."
description: "The durable, append-only journal of pull request, epic queue, plan issue and task changes: table, event types, payloads, idempotency and retention."
---

# Repository change-event journal

ProPR records pull request, epic queue and plan issue changes in a durable, append-only journal: the `repository_events` table. Each event is appended where the change is already persisted, so a coordinator can read one ordered stream per repository. It does not have to poll several endpoints and diff the results.
ProPR records pull request, epic queue, plan issue and task changes in a durable, append-only journal: the `repository_events` table. Each event is appended where the change is already persisted, so a coordinator can read one ordered stream per repository. It does not have to poll several endpoints and diff the results.

The journal is the source of truth. After each append ProPR also publishes a bare `activity:update` event over Socket.IO so the web UI can refresh. That event carries no payload, and a missed one loses nothing: the row is still in the journal.

Expand All @@ -18,9 +18,9 @@ This page covers the storage, the writer and the event catalog. Clients read and
| --- | --- |
| `seq` | `INTEGER PRIMARY KEY AUTOINCREMENT`, used as the cursor. It only increases, and a position is never reused, even after retention deletes rows. |
| `repository` | `owner/repo`. Every read is scoped to one repository. |
| `domain` | `pull_request`, `epic_queue` or `plan_issue`. |
| `domain` | `pull_request`, `epic_queue`, `plan_issue` or `task`. |
| `type` | The event type within the domain (see below). |
| `entity_id` | PR number, epic execution id, or plan issue id, as text. |
| `entity_id` | PR number, epic execution id, plan issue id, or task id, as text. |
| `pr_number`, `issue_number`, `plan_id`, `head_sha` | Optional identifiers to filter on. `plan_id` is the plan (draft) id. |
| `payload` | Small JSON object (at most 4 KiB). |
| `dedupe_key` | Optional key, unique per repository. |
Expand All @@ -30,6 +30,8 @@ Indexes: `(repository, seq)`, `(repository, domain, entity_id, seq)`, and a uniq

`repository_event_floors` stores, for each repository, one past the highest `seq` that retention has deleted (see [Retention](#retention)).

The `domain` column has a CHECK constraint listing the domains; the `task` domain was added by rebuilding the table, keeping every row, its `seq` and the AUTOINCREMENT high-water mark, so existing cursors stay valid.

Rows are written by explicit application appends, not SQLite triggers. A later migration that rebuilds a source table therefore has nothing to recreate. This is different from `goal_events`.

## Writer
Expand All @@ -44,7 +46,7 @@ Rows are written by explicit application appends, not SQLite triggers. A later m

## Event types

Payloads carry identifiers and small scalars only. They never include PR bodies, comment text, diffs or log excerpts. The free-text reasons below are short, system-generated explanations, capped at 300 characters.
Payloads carry identifiers and small scalars only. They never include PR bodies, comment text, diffs, prompts, agent or tool output, file contents or log excerpts. The free-text reasons below are short, system-generated explanations, capped at 300 characters.

### `pull_request`

Expand Down Expand Up @@ -86,6 +88,40 @@ A `blocked` event is appended only when the explanation changes, so rewriting th

Each writer reads the rows it changes, with their prior status, in the same transaction as its guarded update and the append. One event is written per changed row, and nothing for rows its filters excluded or rows already at the new status (a redelivered webhook). There is no dedupe key, so a transition that repeats within one clock tick is still its own event. These writers already publish a `plan` activity event, so the journal does not publish a second one.

### `task`

`entity_id` is the task id. `issue_number` holds the issue of an implementation task; `pr_number` holds the pull request of a follow-up, review or merge-conflict task. Events are appended by the task state manager (`WorkerStateManager`) right after it records the state change in `task_history`, so every job path journals the same way (`packages/core/src/services/taskJournalEvents.ts`).

| Type | Written when | Dedupe key |
| --- | --- | --- |
| `queued` | The task state is created (`pending`), or the task returns to `pending` | `task:{taskId}:queued:v{version}` |
| `started` | The task leaves `pending` for a running state, or a failed task is retried | `task:{taskId}:started:v{version}` |
| `completed`, `failed`, `cancelled` | The task reaches that terminal state | `task:{taskId}:{type}:v{version}` |

`version` is the task state's compare-and-set revision, so a republished transition appends nothing. A queue handoff (a `requeued` or `rescheduled` job recorded as `cancelled` without a terminal reason) is bookkeeping, not an end: it appends no `cancelled`, and resuming it appends no second `started`. A retry appends `started` and a new terminal event again.

Every `task` payload has:

| Field | Meaning |
| --- | --- |
| `taskId` | The task id. |
| `kind` | `implementation` (an issue task), `follow-up` (a pull request comment task), `review`, `merge-conflict` or `automation` (an automation run). Other task types, such as goals and task imports, are not journaled. |
| `issueNumber`, `prNumber` | The issue or pull request the task works on; the other is `null`. |
| `agent`, `model` | The agent alias and model the task was queued with, or `null`. |
| `attempts` | Retry attempts recorded so far. |

Terminal events (`completed`, `failed`, `cancelled`) add:

| Field | Meaning |
| --- | --- |
| `status` | The terminal state, the same as the event type. |
| `terminalReason` | The coded terminal reason, for example `timed_out`, `stalled`, `blocked`, `cancelled_by_user`, `cancelled_pr_closed`; `null` when none was recorded. |
| `durationMs` | Milliseconds from the run's `started` event to the terminal state, across any queue handoffs in between; `null` when the task never started. |
| `pullRequest` | `opened` (the task opened a pull request, including a continuation pull request), `updated` (it pushed a commit to an existing one) or `null`. |
| `failureCategory` | On `failed` only: the coded terminal reason, else the recorded error category (for example `claude_execution`, `git_operation`, `worker`), else `unknown`. Free-text categories are reported as `unknown`. `null` on other events. |

Payloads are built from identifiers and coded fields only. They never include the task's reason text, error message, prompt, agent or tool output, comment bodies, logs, notification recaps or file contents. A task event has no activity mapping: the task state update already publishes its own realtime event. Like every append, a failed task event is logged and the task carries on with the same outcome.

## Realtime

The activity envelope (`packages/shared/src/activityEvents.ts`) has a `pull_request` domain. Each event mapping is shown below:
Expand All @@ -102,7 +138,7 @@ The activity envelope (`packages/shared/src/activityEvents.ts`) has a `pull_requ

## Retention

The daemon's hourly maintenance sweep (the same one that reclaims stale draft context) deletes events older than 30 days, and only those: an unexpired event survives even if an older-stamped event (a delayed delivery, or an outcome stamped with its merge time) was appended after it. For each pruned repository it stores `floor_seq` in `repository_event_floors`. That value is one past the highest deleted `seq`, and it never moves backwards.
The daemon's hourly maintenance sweep (the same one that reclaims stale draft context) deletes events of every domain, `task` included, older than 30 days, and only those: an unexpired event survives even if an older-stamped event (a delayed delivery, or an outcome stamped with its merge time) was appended after it. For each pruned repository it stores `floor_seq` in `repository_event_floors`. That value is one past the highest deleted `seq`, and it never moves backwards.

A reader holding cursor `c` for a repository may have missed pruned events when `c < floor_seq - 1`. Unexpired events below the floor stay readable. A `fromStart` read pages through them with cursors that also carry the repository's retention generation (a digest of its `repository_event_floors` rows); such a cursor stays valid below the floor until retention next prunes the repository, which rewrites `pruned_at` even when the floor does not move. This tells an expired cursor apart from an empty result. A repository that has never been pruned has floor `1`.

Expand Down
21 changes: 20 additions & 1 deletion docs/docs/features/agent-extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,24 @@ revision. Enabling or disabling an item carries the sealed credentials into the
revision unchanged, without decrypting them. Removing or renaming an item that an automation adds explicitly is
refused with a message naming those automations; remove it from them first.

### Managed items

An installed [plugin](./plugins.md#what-an-enabled-plugin-adds) adds its MCP
servers and skills to the library as **managed items**, tagged with the plugin
id (`managedBy: { plugin, item, active }`, plus `registrations` on an OAuth
server: the client registration methods its sign-in may use; shown as a **From plugin:** *plugin name* badge linking to the
plugin in the settings screen, where only those fields are editable). Their definition comes from the plugin manifest and is
read-only: a save may change only `enabled`, `repositories`, `agents` and
`autoApply`. Saves that change anything else on a managed item (for a package
server, also its package, version, integrity value, arguments and variables),
remove it or add an item with `managedBy` are refused, and so is replacing its
credential: a managed item's credential or secret variables come from the
plugin's connections. The plugin's own state wins over the item's: while the
plugin is disabled, or a package server waits for a new acknowledgement after
an upgrade, the item is inactive (`active: false`) and applies to no run
whatever its enabled switch says. Uninstalling the plugin removes its managed
items.

## Which runs use extensions

At launch, Core resolves the run's extension set against the **current** library
Expand Down Expand Up @@ -594,7 +612,7 @@ safe to re-run, and never blocks startup):
marked not configured and logs a warning; replace it in Settings.

The library holds at most 32 servers and 32 skill bundles, and an item at most
64 repositories and 64 agents. If the previous configuration does not fit (for
256 repositories and 64 agents. If the previous configuration does not fit (for
example five bundles of eight distinct servers each), nothing is dropped: the
copy is not committed, the startup log lists the items that do not fit, and
it retries at the next start. Until it completes, automations that selected a
Expand Down Expand Up @@ -633,3 +651,4 @@ Skill-bearing runs require a Linux worker with `/proc/self/fd` available for sec

- [MCP Connections and Operator Tools](/docs/features/mcp)
- [Agents and Models](/docs/features/agents-and-models)
- [Plugins](/docs/features/plugins): official integrations that bundle MCP servers, skills and automation templates
Loading
Loading