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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 92 additions & 50 deletions .github/scripts/bump-plugin-pins.sh
Original file line number Diff line number Diff line change
@@ -1,21 +1,34 @@
#!/usr/bin/env bash
#
# Resolve the latest released server/client/tools versions and update the
# demarkus-memory plugin's binary pins (and its patch version) to match.
# Resolve the latest released server/client/tools versions and update the demarkus
# plugins' binary pins to match. Since the Approach-C refactor the pins live in
# TWO places that must stay in lockstep:
#
# Idempotent: run it as often as you like. It writes "changed=true" plus the
# resolved versions to $GITHUB_OUTPUT (and stdout) when it edited files, and
# "changed=false" when everything was already current. Requires `gh` and `jq`.
# 1. tools/demarkus-plugin/internal/provision/provision.go — the Go consts
# serverVersion / clientVersion / toolsVersion (source of truth; what the
# binary provisions and self-updates to).
# 2. plugins/*/scripts/bootstrap.sh — TOOLS_VERSION (the chicken-and-egg pin
# the tiny installer needs to fetch the demarkus-plugin binary itself).
#
# Env: GH_REPO (default latebit-io/demarkus), GH_TOKEN for `gh`.
# toolsVersion and every bootstrap TOOLS_VERSION MUST match, or provision and
# bootstrap fight over which demarkus-plugin to install.
#
# It also patch-bumps the affected plugins' versions, in two lineages:
# - memory (changes when server|client|tools change): claude-code +
# pi-memory + the claude-code marketplace entry.
# - knowledge (changes when tools changes, via its bootstrap): claude-code-
# knowledge + pi-knowledge + the claude-code-knowledge marketplace entry.
# (The Codex plugins carry no version file — only a bootstrap pin.)
#
# Idempotent. Writes "changed=true" plus the resolved versions to $GITHUB_OUTPUT
# (and stdout) when it edited files, "changed=false" otherwise. Requires gh + jq.
#
# Env: GH_REPO (default latebit-io/demarkus), GH_TOKEN for gh.
set -euo pipefail

repo="${GH_REPO:-latebit-io/demarkus}"
lib="plugins/claude-code/scripts/lib.sh"
plugin_json="plugins/claude-code/.claude-plugin/plugin.json"
provision="tools/demarkus-plugin/internal/provision/provision.go"
marketplace=".claude-plugin/marketplace.json"
# The marketplace entry this plugin owns, keyed by its source path.
plugin_source="./plugins/claude-code"

# latest <module> -> "X.Y.Z" (highest semver among that module's release tags).
latest() {
Expand All @@ -24,15 +37,53 @@ latest() {
| sed "s#$1/v##" | sort -V | tail -1
}

# pin <CONST> -> current value of `readonly CONST="..."` in lib.sh.
pin() { sed -nE "s/^readonly $1=\"([^\"]+)\".*/\1/p" "$lib"; }
# goconst <NAME> -> current value of `NAME = "..."` in provision.go.
goconst() { sed -nE "s/^[[:space:]]*$1[[:space:]]*=[[:space:]]*\"([^\"]+)\".*/\1/p" "$provision"; }

emit() {
echo "$1=$2"
[[ -n "${GITHUB_OUTPUT:-}" ]] && echo "$1=$2" >>"$GITHUB_OUTPUT"
return 0
}

# set_goconst <NAME> <value> — portable in-place edit (temp file; no sed -i).
set_goconst() {
local tmp; tmp=$(mktemp)
sed -E "s/^([[:space:]]*$1[[:space:]]*=[[:space:]]*)\"[^\"]+\"/\1\"$2\"/" "$provision" >"$tmp" && mv "$tmp" "$provision"
}

# set_bootstrap_tools <value> — update TOOLS_VERSION in every plugin bootstrap.
set_bootstrap_tools() {
local f tmp
for f in plugins/*/scripts/bootstrap.sh; do
[[ -f "$f" ]] || continue
tmp=$(mktemp)
sed -E "s/^(TOOLS_VERSION=)\"[^\"]+\"/\1\"$1\"/" "$f" >"$tmp" && mv "$tmp" "$f"
done
}

# bump_json_version <file> — patch-bump the .version field in a JSON file. Echoes
# the new version. Uses jq (these files round-trip cleanly through it).
bump_json_version() {
local f cur new tmp; f="$1"
cur=$(jq -r '.version' "$f")
new=$(awk -F. '{printf "%d.%d.%d", $1, $2, $3 + 1}' <<<"$cur")
tmp=$(mktemp)
jq --arg v "$new" '.version = $v' "$f" >"$tmp" && mv "$tmp" "$f"
echo "$new"
}

# set_marketplace_version <source-path> <value> — set the version of the
# marketplace entry whose "source" is <source-path>, preserving hand formatting.
set_marketplace_version() {
local tmp; tmp=$(mktemp)
awk -v v="$2" -v src="$1" '
index($0, "\"source\": \"" src "\"") { inblock = 1 }
inblock && /"version":/ { sub(/"version": "[^"]*"/, "\"version\": \"" v "\""); inblock = 0 }
{ print }
' "$marketplace" >"$tmp" && mv "$tmp" "$marketplace"
Comment on lines +49 to +84

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Make the rewrite helpers fail on zero matches.

Lines 49-84 silently succeed when the target key is missing. A renamed Go const, missing TOOLS_VERSION, or drifted marketplace source leaves one pin stale, but the script still reports success and the workflow will open a partial bump PR. Count matches and return non-zero unless each rewrite hits the expected target.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/scripts/bump-plugin-pins.sh around lines 49 - 84, The rewrite
helpers in bump-plugin-pins.sh currently succeed even when no target is updated,
which can leave stale pins undetected. Update set_goconst, set_bootstrap_tools,
bump_json_version, and set_marketplace_version to verify they actually matched
and rewrote the expected entry (for example by checking match counts or
comparing before/after content) and return a non-zero status when a target key,
TOOLS_VERSION, .version field, or marketplace source block is missing. Keep the
existing function names and preserve their current edit behavior, but make the
workflow fail fast instead of reporting success on partial or no-op rewrites.

}

new_server=$(latest server)
new_client=$(latest client)
new_tools=$(latest tools)
Expand All @@ -41,9 +92,9 @@ if [[ -z "$new_server" || -z "$new_client" || -z "$new_tools" ]]; then
exit 1
fi

cur_server=$(pin SERVER_VERSION)
cur_client=$(pin CLIENT_VERSION)
cur_tools=$(pin TOOLS_VERSION)
cur_server=$(goconst serverVersion)
cur_client=$(goconst clientVersion)
cur_tools=$(goconst toolsVersion)

emit server "$new_server"
emit client "$new_client"
Expand All @@ -55,39 +106,30 @@ if [[ "$new_server" == "$cur_server" && "$new_client" == "$cur_client" && "$new_
exit 0
fi

# Update the three pins. Write via a temp file rather than `sed -i`, whose flag
# syntax differs between GNU and BSD sed — this stays portable.
tmp=$(mktemp)
sed -E \
-e "s/^(readonly SERVER_VERSION=)\"[^\"]+\"/\1\"$new_server\"/" \
-e "s/^(readonly CLIENT_VERSION=)\"[^\"]+\"/\1\"$new_client\"/" \
-e "s/^(readonly TOOLS_VERSION=)\"[^\"]+\"/\1\"$new_tools\"/" \
"$lib" >"$tmp" && mv "$tmp" "$lib"

# Patch-bump the plugin version, kept in lockstep between plugin.json and the
# marketplace entry. jq targets the exact entry by source path, so a coincident
# version on another plugin is never touched.
cur_ver=$(jq -r '.version' "$plugin_json")
new_ver=$(awk -F. '{printf "%d.%d.%d", $1, $2, $3 + 1}' <<<"$cur_ver")

# plugin.json round-trips cleanly through jq (targets .version exactly).
tmp=$(mktemp)
jq --arg v "$new_ver" '.version = $v' "$plugin_json" >"$tmp" && mv "$tmp" "$plugin_json"

# marketplace.json is edited with awk to preserve its hand-authored formatting
# (jq would expand the inline tag arrays). Anchor on this plugin's exact source
# path (literal substring match), then replace the next version line in that
# entry only — never another plugin's.
tmp=$(mktemp)
awk -v v="$new_ver" -v src="$plugin_source" '
index($0, "\"source\": \"" src "\"") { inblock = 1 }
inblock && /"version":/ {
sub(/"version": "[^"]*"/, "\"version\": \"" v "\"")
inblock = 0
}
{ print }
' "$marketplace" >"$tmp" && mv "$tmp" "$marketplace"

emit version "$new_ver"
# Update the Go consts (source of truth) and every bootstrap's TOOLS_VERSION.
set_goconst serverVersion "$new_server"
set_goconst clientVersion "$new_client"
set_goconst toolsVersion "$new_tools"
[[ "$new_tools" != "$cur_tools" ]] && set_bootstrap_tools "$new_tools"

# Patch-bump the affected lineages.
memory_changed=false
knowledge_changed=false
[[ "$new_server" != "$cur_server" || "$new_client" != "$cur_client" || "$new_tools" != "$cur_tools" ]] && memory_changed=true
[[ "$new_tools" != "$cur_tools" ]] && knowledge_changed=true

if [[ "$memory_changed" == true ]]; then
mv_ver="$(bump_json_version plugins/claude-code/.claude-plugin/plugin.json)"
set_marketplace_version "./plugins/claude-code" "$mv_ver"
_=$(bump_json_version plugins/pi-memory/package.json)
emit memory_version "$mv_ver"
fi
if [[ "$knowledge_changed" == true ]]; then
kv_ver="$(bump_json_version plugins/claude-code-knowledge/.claude-plugin/plugin.json)"
set_marketplace_version "./plugins/claude-code-knowledge" "$kv_ver"
_=$(bump_json_version plugins/pi-knowledge/package.json)
emit knowledge_version "$kv_ver"
fi

emit changed true
echo "bumped: server $cur_server->$new_server, client $cur_client->$new_client, tools $cur_tools->$new_tools, plugin $cur_ver->$new_ver"
echo "bumped: server $cur_server->$new_server, client $cur_client->$new_client, tools $cur_tools->$new_tools"
22 changes: 15 additions & 7 deletions .github/workflows/plugin-pin-bump.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,30 +41,38 @@ jobs:
SERVER: ${{ steps.bump.outputs.server }}
CLIENT: ${{ steps.bump.outputs.client }}
TOOLS: ${{ steps.bump.outputs.tools }}
VERSION: ${{ steps.bump.outputs.version }}
MEMORY_VERSION: ${{ steps.bump.outputs.memory_version }}
KNOWLEDGE_VERSION: ${{ steps.bump.outputs.knowledge_version }}
run: |
set -euo pipefail
branch="auto/plugin-pin-bump"
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"

git checkout -B "$branch"
git add plugins/claude-code/scripts/lib.sh \
# Pins live in the Go consts + every plugin's bootstrap; versions in the
# plugin manifests + marketplace. Stage all of them (the bump script only
# touches these paths).
git add tools/demarkus-plugin/internal/provision/provision.go \
plugins/*/scripts/bootstrap.sh \
plugins/claude-code/.claude-plugin/plugin.json \
plugins/claude-code-knowledge/.claude-plugin/plugin.json \
plugins/pi-memory/package.json \
plugins/pi-knowledge/package.json \
.claude-plugin/marketplace.json
git commit -m "chore(plugin-memory): bump managed binaries to latest release" \
-m "server ${SERVER}, client ${CLIENT}, tools ${TOOLS}; plugin v${VERSION}."
git commit -m "chore(plugins): bump managed binaries to latest release" \
-m "server ${SERVER}, client ${CLIENT}, tools ${TOOLS}."
git push --force origin "$branch"

title="chore(plugin-memory): bump managed binaries (server ${SERVER})"
title="chore(plugins): bump managed binaries (server ${SERVER})"
body=$(printf '%s\n' \
"Automated bump of the demarkus-memory plugin's managed-binary pins to the latest releases:" \
"Automated bump of the demarkus plugins' managed-binary pins (Go consts in provision.go + every bootstrap's TOOLS_VERSION) to the latest releases:" \
"" \
"- server \`${SERVER}\`" \
"- client \`${CLIENT}\`" \
"- tools \`${TOOLS}\`" \
"" \
"Plugin patch-bumped to \`v${VERSION}\`. Review and merge to ship; drift-detection re-downloads on the next plugin-managed server spawn." \
"Plugins patch-bumped: memory \`${MEMORY_VERSION:-unchanged}\`, knowledge \`${KNOWLEDGE_VERSION:-unchanged}\`. Review and merge to ship; drift-detection re-downloads on the next plugin-managed server spawn." \
"" \
"Generated by \`.github/workflows/plugin-pin-bump.yml\`.")

Expand Down
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,10 @@ modified: 2026-01-15T10:30:00Z

- [Caztor](https://github.com/kevinboone/caztor): cross-platform Java GUI browser with preliminary Demarkus support
- [Obsidian plugin](https://github.com/latebit-io/obsidian-demarkus): publish and browse from Obsidian
- [Claude Code memory plugin](plugins/claude-code/): zero-config local memory for Claude Code
- [Claude Code knowledge plugin](plugins/claude-code-knowledge/): join an organizational knowledge system (broker-fronted, MCP OAuth)
- Coding-agent plugins — same memory + knowledge behavior across harnesses, sharing one decision engine (see [plugins/README.md](plugins/README.md)):
- Claude Code: [memory](plugins/claude-code/) · [knowledge](plugins/claude-code-knowledge/)
- pi: [memory](plugins/pi-memory/) · [knowledge](plugins/pi-knowledge/)
- Codex: [memory](plugins/codex-memory/) · [knowledge](plugins/codex-knowledge/)
- [OpenClaw skill](https://clawhub.ai/ontehfritz/demarkus): ClawHub skill for OpenClaw agents (see [install guide](https://www.demarkus.io/install/openclaw/))

See [www.demarkus.io/ecosystem](https://www.demarkus.io/ecosystem/) for the full list.
Expand Down
51 changes: 51 additions & 0 deletions plugins/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# demarkus coding-agent plugins

Same demarkus behavior — local **memory** (a "soul") and organizational **knowledge** systems — across multiple coding agents, with **one shared decision engine** and a thin per-harness adapter on top.

## The architecture: one binary, thin adapters

All harness-agnostic logic and orchestration live in a single Go binary, **`demarkus-plugin`** (in [`tools/demarkus-plugin`](../tools/demarkus-plugin); shipped in the `tools/` release, installed to `~/.demarkus/bin` by each plugin's `scripts/bootstrap.sh`):

- **`gate`** — the write-time publish tag-gate + destination gate + knowledge axes/fields gate.
- **`nudge`** — recall / promote / session-end reminders.
- **`guidance`** — session-start standing context (+ one-time offers, health warnings).
- **`registry`** — locked, atomic reads/mutations of `~/.demarkus` state (souls, knowledge systems, policy mirrors, promote targets, MCP config).
- **`provision`** / **`mcp-serve`** — managed-server lifecycle and the local-soul MCP launcher.

Each **plugin is a thin adapter**: a manifest (its harness's hook/event wiring), one-line hook scripts that pipe the harness's hook JSON to the binary with a `--format` for that harness, and the shared prompt content (commands + standing guidance). A fix in the binary fixes every harness at once; **a new harness reimplements nothing** — it only re-expresses the wiring.

```
harness hook/event ──▶ thin adapter ──▶ demarkus-plugin <cmd> --format <harness> ──▶ decision ──▶ adapter translates back
(all logic here, shared)
```
Comment on lines +17 to +20

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add a language to this fenced block.

Lines 17-20 use a bare fence, which trips markdownlint MD040 on this file. text is enough here.

Patch sketch
-```
+```text
 harness hook/event ──▶ thin adapter ──▶ demarkus-plugin <cmd> --format <harness> ──▶ decision ──▶ adapter translates back
                                               (all logic here, shared)
</details>

<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion

🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 17-17: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugins/README.md` around lines 17 - 20, Add a language identifier to the
fenced code block in the README so markdownlint MD040 is satisfied. Update the
bare fence around the harness flow diagram in the markdown file to use a text
fence, keeping the content unchanged and preserving the existing diagram layout.

Source: Linters/SAST tools


## Harness matrix

| Harness | Memory | Knowledge | Wiring | Gate output format |
|---|---|---|---|---|
| Claude Code | [`claude-code`](claude-code/) | [`claude-code-knowledge`](claude-code-knowledge/) | `plugin.json` hooks + marketplace | `claude-pre` / `claude-post` / `claude` |
| pi | [`pi-memory`](pi-memory/) | [`pi-knowledge`](pi-knowledge/) | TS extension (`src/`) + `package.json` | `json` (native) |
| Codex | [`codex-memory`](codex-memory/) | [`codex-knowledge`](codex-knowledge/) | `config.toml` hooks + `mcp_servers` | `codex-pre` / `codex-post` / `codex` |

The three harnesses' hook contracts converge: Claude Code and Codex share the same `hookSpecificOutput` / `{decision,reason}` schema and event names (`SessionStart`, `PreToolUse`, `PostToolUse`, `UserPromptSubmit`, `Stop`), so the binary's `claude*` and `codex*` formats differ only where the harnesses do (Codex's `PreToolUse` has no `ask`, so `codex-pre` folds `ask` into `deny` with a "confirm first" reason). pi consumes the native `json` decision directly in TypeScript.

## Install

- **Claude Code** — add the marketplace (`.claude-plugin/marketplace.json`) and enable `demarkus-memory` / `demarkus-knowledge`.
- **pi** — `pi install git:github.com/latebit-io/demarkus-pi-memory` (and `…-pi-knowledge`); the standalone repos are mirrored from this monorepo. See each plugin's README.
- **Codex** — run `plugins/codex-memory/install.sh` (and/or `codex-knowledge/install.sh`) from a checkout; it bootstraps the binary, writes a managed block into `~/.codex/config.toml`, and installs the command procedures as skills under `~/.agents/skills/` (invoke with `/skills` or `$name`).

All three share the same `~/.demarkus` state on a machine — a soul joined or policy set under one harness is visible to all.

## Adding a new harness

The Codex plugins are the worked proof that a new harness is adapter-only. To add one:

1. **Wire the harness's lifecycle** to the binary's subcommands: session-start → `provision` (memory only) + `guidance`; pre-write → `gate`; post-write → `gate` (warn reminder) + `nudge` (promote); prompt → `nudge` (recall); session-end → `nudge` (session-end).
2. **Pick or add an output format.** If the harness's hook schema matches Claude/Codex, reuse `claude*`/`codex*`. Otherwise add a small `--format` case in [`tools/demarkus-plugin/main.go`](../tools/demarkus-plugin/main.go) (`emitDecision` for the gate; the `cmdNudge`/`cmdGuidance` branches for the rest) — this is the only Go change a new harness should need.
3. **Wire MCP**: launch the local soul via `demarkus-plugin mcp-serve`; add knowledge brokers per the harness's MCP mechanism.
4. **Reuse the prompt content** (`context/session-guidance.md` + the command markdown), adapting only the harness-specific MCP-registration steps in the join prompts.

## Versioning & pins

The managed-binary pins (server / client / tools) are the Go consts in [`provision.go`](../tools/demarkus-plugin/internal/provision/provision.go); `toolsVersion` must match the `TOOLS_VERSION` in every plugin's `bootstrap.sh`. The [`plugin-pin-bump.yml`](../.github/workflows/plugin-pin-bump.yml) workflow keeps them current and patch-bumps the affected plugin versions.
Loading