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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 56 additions & 13 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,17 @@ on:
push:
tags: ['v*']
workflow_dispatch:
inputs:
replace_existing:
description: Replace the existing release for the current package version from main
type: boolean
default: false

permissions:
contents: read

concurrency:
group: release-${{ github.ref }}
group: release
cancel-in-progress: false

env:
Expand All @@ -19,9 +24,11 @@ env:

jobs:
verify:
if: github.event.repository.private == false && startsWith(github.ref, 'refs/tags/v')
if: github.event.repository.private == false && (startsWith(github.ref, 'refs/tags/v') || (github.ref == 'refs/heads/main' && inputs.replace_existing))
runs-on: ubuntu-22.04
timeout-minutes: 5
outputs:
tag: ${{ steps.version.outputs.tag }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
Expand All @@ -31,19 +38,28 @@ jobs:
with:
python-version: '3.13'
- name: Require a release version from main
id: version
env:
TAG: ${{ github.ref_name }}
REPLACE_RELEASE: ${{ github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main' && inputs.replace_existing }}
GH_TOKEN: ${{ github.token }}
run: |
python - <<'PY'
import os, re, tomllib
from pathlib import Path
version = tomllib.loads(Path('Cargo.toml').read_text())['package']['version']
assert re.fullmatch(r'[0-9]+\.[0-9]+\.[0-9]+', version), 'Use an approved release version'
assert os.environ['TAG'] == f'v{version}', 'Tag must match Cargo.toml'
tag = f'v{version}'
if os.environ['REPLACE_RELEASE'] != 'true':
assert os.environ['TAG'] == tag, 'Tag must match Cargo.toml'
with open(os.environ['GITHUB_OUTPUT'], 'a') as output:
output.write(f'tag={tag}\n')
PY
TAG=$(sed -n 's/^tag=//p' "$GITHUB_OUTPUT")
git merge-base --is-ancestor "$GITHUB_SHA" origin/main
if gh release view "$TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
if [ "$REPLACE_RELEASE" = true ]; then
test "$(gh release view "$TAG" --repo "$GITHUB_REPOSITORY" --json isImmutable --jq .isImmutable)" = false
elif gh release view "$TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
echo 'This release already exists; do not replace its artifacts.' >&2
exit 1
fi
Expand All @@ -53,7 +69,7 @@ jobs:
uses: ./.github/workflows/ci.yml

build:
needs: test
needs: verify
name: Build (${{ matrix.target }})
runs-on: ${{ matrix.os }}
timeout-minutes: 25
Expand Down Expand Up @@ -94,25 +110,52 @@ jobs:
retention-days: 7

publish:
needs: build
needs: [verify, test, build]
runs-on: ubuntu-22.04
timeout-minutes: 5
timeout-minutes: 10
permissions:
contents: write
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
TAG: ${{ needs.verify.outputs.tag }}
REPLACE_RELEASE: ${{ github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main' && inputs.replace_existing }}
steps:
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: binary-*
merge-multiple: true
path: dist
- name: Preserve the previous release before replacement
if: env.REPLACE_RELEASE == 'true'
run: |
mkdir previous
gh release view "$TAG" --json name,body,tagName,targetCommitish,isDraft,isPrerelease,isImmutable > previous/release.json
gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/$TAG" > previous/tag.json
gh release download "$TAG" --dir previous
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: env.REPLACE_RELEASE == 'true'
with:
name: previous-release
path: previous/*
if-no-files-found: error
retention-days: 7
- name: Publish the verified binaries and checksums
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
TAG: ${{ github.ref_name }}
RELEASE_NOTES: 'Native MCP server for Windows, macOS, and Linux. Give your agent https://github.com/mcpads/dotmend and ask it to follow INSTALL.md. Agents briefly explain the available human controls and the one-stroke Undo limit when sharing the screen. Requires stateless MCP 2026-07-28. The browser interface and editing guide are embedded in each executable. SHA256SUMS covers all binary archives.'
run: |
cd dist
sha256sum -- *.tar.gz *.zip > SHA256SUMS
gh release create "$TAG" --verify-tag --title "Dotmend ${TAG#v}" \
--notes 'Native MCP server for Windows, macOS, and Linux. Give your agent https://github.com/mcpads/dotmend and ask it to follow INSTALL.md. Requires stateless MCP 2026-07-28. The browser interface and editing guide are embedded in each executable. SHA256SUMS covers all binary archives.' \
--latest -- ./*.tar.gz ./*.zip SHA256SUMS
if [ "$REPLACE_RELEASE" = true ]; then
gh release edit "$TAG" --draft=true
tag_object=$(gh api --method POST "repos/$GITHUB_REPOSITORY/git/tags" \
-f tag="$TAG" -f message="Dotmend ${TAG#v}" -f object="$GITHUB_SHA" -f type=commit --jq .sha)
gh api --method PATCH "repos/$GITHUB_REPOSITORY/git/refs/tags/$TAG" \
-f sha="$tag_object" -F force=true >/dev/null
gh release upload "$TAG" --clobber -- ./*.tar.gz ./*.zip SHA256SUMS
gh release edit "$TAG" --draft=false --latest --verify-tag \
--target "$GITHUB_SHA" --title "Dotmend ${TAG#v}" --notes "$RELEASE_NOTES"
else
gh release create "$TAG" --verify-tag --title "Dotmend ${TAG#v}" \
--notes "$RELEASE_NOTES" --latest -- ./*.tar.gz ./*.zip SHA256SUMS
fi
4 changes: 3 additions & 1 deletion resources/agent-usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,16 @@ Art coordinates start at the top-left `(0,0)`, with `+x` right and `+y` down. Th

Humans paint with palette colors, mark suspected issues, undo the last stroke once and save. Handle collections, filters, past candidates, regions and reviews through conversation and tools.

When first sharing the screen, briefly explain its available controls in the user's language: choose a palette color, then click or drag to paint; **Mark issues** uses left clicks/drags to add marks and right clicks/drags to remove them; **Undo** (Ctrl/Cmd+Z) reverses the last stroke once; **Save** (Ctrl/Cmd+S) records the current result. A click or drag is one stroke, and painting and marking share the same undo. Tell the user to ask you for older results or other operations. Adapt the explanation to read-only and playback views; repeat it only if asked or needed. Do not send the user to this guide or require IDs and technical settings.

1. Choose a fresh random `control_id` per independent task (16..128 ASCII letters, digits, underscores or hyphens); retain it across calls and connections. Call `open_workbench({control_id})` for the instance ID and URL. Do not bypass another owner or the shared limit. Read `inspect_presentation({})`; no existing view returns `presentation:null`.
2. Call `present_art({control_id,workbench_id,view})`. Set `view.title`, `note` and `items` to describe the user's task. An art item is `{kind:"art",art_id,label,region,scale,editable,request_id?}`. Attach `request_id` to an editable item carrying out a protected request.
3. Copy the freshly read IDs into `view.expected_presentation_id` and `view.expected_state_id`. Both keys must be explicit `null` for the first view. On conflict, read the human's latest edits first.
4. Read the explicit save from `inspect_presentation` at `saved.art_ids` and `saved.state_id`. Current drafts are at `state.art_ids`; `dirty` distinguishes them from the last explicit save. Saving is not acceptance.

Do not close the screen while the human is working. Finish with `close_workbench({control_id,workbench_id})` and check its result. `inspect_workbench({control_id})` distinguishes `owned`, `closing`, `busy` and `closed` without renewing activity. Reopening with the same control_id reuses its ID and URL and renews activity; it does not change its timeout. Successful presentation and human actions renew activity; polling does not. The default idle timeout is 1800 seconds, configurable from 1 to 1800. HTTP ends when the MCP process hosting it exits or the idle deadline expires; art and drafts remain.

One explicit controller owns each workspace's screen, with a shared maximum of four. The same control_id works across MCP connections; different tasks remain separate even on one connection. Share only the workbench URL with the human. Independent candidate creation does not require screen control. Never bypass control using shell servers, alternate runtime directories or lock-file changes. The local HTTP interface is not an MCP transport or a public agent mutation API. Closing a forwarding MCP connection does not close the host's screen. If close returns closing, inspect until closed.
One explicit controller owns each workspace's screen, with a shared maximum of four. The same control_id works across MCP connections; different tasks remain separate even on one connection. Share the workbench URL with the human; keep control IDs private. Independent candidate creation does not require screen control. Never bypass control using shell servers, alternate runtime directories or lock-file changes. The local HTTP interface is not an MCP transport or a public agent mutation API. Closing a forwarding MCP connection does not close the host's screen. If close returns closing, inspect until closed.

For collections and filters, use `list_art` and present the selected candidates in order. For history, inspect parent IDs or archived presentations and states. `inspect_presentation({presentation_id,state_id})` is read-only; use `present_art` to change the visible screen. A response with `is_current:false` is not the current screen.

Expand Down
4 changes: 2 additions & 2 deletions src/server_instructions.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
pub const INSTRUCTIONS: &str = r#"Create palette-constrained retro game art; you are the user's interface. tools/list defines exact inputs/outputs; dotmend://guides/editing gives procedures. MCP uses request metadata, never connection history.

WORKBENCH
Choose a fresh random control_id per independent task; retain it across calls and connections. Pass it to open_workbench, inspect_workbench, close_workbench and present_art. Reopening with the same control_id reuses the instance. Also pass its workbench_id to present_art {control_id,workbench_id,view} and close_workbench; share only the URL with the human.
Choose a fresh random control_id per independent task; retain it across calls and connections. Pass it to open_workbench, inspect_workbench, close_workbench and present_art. Reopening with the same control_id reuses the instance. Also pass its workbench_id to present_art {control_id,workbench_id,view} and close_workbench. Share the URL with the human; keep control IDs private.
One controller per workspace; four workbenches per shared runtime directory. Never bypass control or limits with shell servers, PIDs or alternate paths. HTTP ends when its hosting MCP process exits or its returned idle timeout expires; polling does not renew activity. Close when finished, never while the human is working. Art and drafts persist.

HUMAN SCREEN
Humans paint, mark issues, undo once and save. Prepare collections, filters, past art, reference crops and known-time playback yourself.
At first handoff, briefly explain available controls in the user's language: palette + click/drag paints; Mark issues: left adds, right removes; Undo once for the last stroke (Ctrl/Cmd+Z); Save (Ctrl/Cmd+S). Offer older results through conversation. Repeat only when needed. Prepare collections, filters, past art, reference crops and known-time playback yourself.
Read inspect_presentation; copy current IDs into view.expected_presentation_id/expected_state_id (both explicit null only for the first view). saved.art_ids holds the explicit save; state.art_ids may contain a newer draft. state.concerns and saved.concerns identify marked candidates and pixels; marks request observation, not edit permission or rejection.

EDIT AND RECOVER
Expand Down