diff --git a/CHANGELOG.md b/CHANGELOG.md
index 12f66a3..51464ba 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,13 @@
# Changelog
+## 0.5.1
+
+- Teach posts to restore returning participants on open with
+ `pagelike.identity()`, while keeping first-time and signed-out viewers anonymous.
+- Show an existing choice in the vote example before another click, and include
+ a browser check that opening the example writes no contribution.
+- Refresh both distributions from Control's verified public skill snapshot.
+
## 0.5.0
- Extract the creator plugin into its own public, MIT-licensed repository.
diff --git a/directory/differ/.claude-plugin/plugin.json b/directory/differ/.claude-plugin/plugin.json
index b8dd6d0..3819fa7 100644
--- a/directory/differ/.claude-plugin/plugin.json
+++ b/directory/differ/.claude-plugin/plugin.json
@@ -1,6 +1,6 @@
{
"name": "differ",
- "version": "0.5.0",
+ "version": "0.5.1",
"description": "Create, preview, publish and revise interactive social posts.",
"author": {
"name": "Sky Valley",
diff --git a/directory/differ/.codex-plugin/plugin.json b/directory/differ/.codex-plugin/plugin.json
index e230fb1..eda9218 100644
--- a/directory/differ/.codex-plugin/plugin.json
+++ b/directory/differ/.codex-plugin/plugin.json
@@ -1,6 +1,6 @@
{
"name": "differ",
- "version": "0.5.0",
+ "version": "0.5.1",
"description": "Create, preview, publish and revise interactive social posts.",
"author": {
"name": "Sky Valley",
diff --git a/directory/differ/plugin.json b/directory/differ/plugin.json
index 1459848..25ba32c 100644
--- a/directory/differ/plugin.json
+++ b/directory/differ/plugin.json
@@ -1,7 +1,7 @@
{
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
"name": "differ",
- "version": "0.5.0",
+ "version": "0.5.1",
"description": "Create, preview, publish and revise interactive social posts.",
"author": {
"name": "Sky Valley",
diff --git a/directory/differ/skills.lock.json b/directory/differ/skills.lock.json
index 8eb1f11..83484b0 100644
--- a/directory/differ/skills.lock.json
+++ b/directory/differ/skills.lock.json
@@ -77,8 +77,13 @@
},
{
"uri": "skill://publish-content/examples/vote/README.md",
- "digest": "sha256:136357a5930e8e61fe1f3161db053435d81341526445b0edfda1e815b4cdc2d2",
- "size": 1889
+ "digest": "sha256:d291254b1d3fceea3b3b829a8ff8125b6ed1b1e729bc57576c682a5f9097a3e2",
+ "size": 2589
+ },
+ {
+ "uri": "skill://publish-content/examples/vote/check-browser.mjs",
+ "digest": "sha256:44f2d6c31a07f59ae8cb0ed7655d06309b6cffb38081d27baf27cb6b7d8f5b79",
+ "size": 1819
},
{
"uri": "skill://publish-content/examples/vote/check.mjs",
@@ -87,8 +92,8 @@
},
{
"uri": "skill://publish-content/examples/vote/content/app.mjs",
- "digest": "sha256:e1010cb228523ebdc499d008c15c201b290149dce297192253420dd0e9a3dfc6",
- "size": 1952
+ "digest": "sha256:237ee0de7f0968ec43f19ffc21fd5077e8d0778cf9f49b38d9102a68a167596e",
+ "size": 2321
},
{
"uri": "skill://publish-content/examples/vote/content/data/votes.html",
@@ -97,8 +102,8 @@
},
{
"uri": "skill://publish-content/examples/vote/content/index.html",
- "digest": "sha256:d44bec3347f4c4536a15fc9633ab9f9b4e255618edb6d498d9a8d08be0f99d1d",
- "size": 1131
+ "digest": "sha256:ce0aa62697568b6f1d683b21748f48d7cb9cc1436f87e07099aabfe03f5d1a57",
+ "size": 1168
},
{
"uri": "skill://publish-content/examples/vote/content/rules.html",
@@ -107,8 +112,8 @@
},
{
"uri": "skill://publish-content/examples/vote/content/vote.mjs",
- "digest": "sha256:3ce07820bfba8d8792bbea28671d7d835b3e3c5a1011e65ec7215d999bb22502",
- "size": 894
+ "digest": "sha256:1eeac58ec35de44f18b68064a0de2da6e30e13d6486a7c29f197397295f23fa1",
+ "size": 978
},
{
"uri": "skill://publish-content/references/connector.md",
@@ -122,8 +127,8 @@
},
{
"uri": "skill://publish-content/references/participation.md",
- "digest": "sha256:a4f05532cd9159907852fe1795e12705acd2deb0ec05acf2135fd2385ac43271",
- "size": 7620
+ "digest": "sha256:b8ff86ab2003b6ad5a4f044f420f428d190f2478c64a9576019d17197dc67513",
+ "size": 8108
},
{
"uri": "skill://publish-content/references/setup.md",
diff --git a/directory/differ/skills/publish-content/examples/vote/README.md b/directory/differ/skills/publish-content/examples/vote/README.md
index 3845e40..99547ea 100644
--- a/directory/differ/skills/publish-content/examples/vote/README.md
+++ b/directory/differ/skills/publish-content/examples/vote/README.md
@@ -1,7 +1,7 @@
# The afternoon question
A complete ordinary HTML/JS post: tea or coffee, with one effective choice per
-signed-in identity, changeable over time. No SDK, generated rules or external
+signed-in identity, changeable over time. Uses the managed identity SDK, with no generated rules or external
database. Publish only `examples/vote/content/`.
Read `examples/vote/content/index.html`, `app.mjs`, `vote.mjs`, `rules.html` and
@@ -14,6 +14,11 @@ restricts choices. The browser counts the latest remaining choice for each
trusted identity. Repeated clicks do not increase that identity's effective
vote. This is one vote per identity, not a guarantee about physical people.
+On load, `pagelike.identity()` quietly restores an existing post identity. The
+UI then marks its latest remaining choice before any click. A new or signed-out
+visitor stays anonymous. Opening does not append a vote; `participate()` and
+the write remain in the button handler.
+
History is deliberate: removing the latest choice reveals an earlier remaining
one. Player's Your participation removes individual contributions; removing all
of your choices leaves no vote. The creator can moderate contributions. The UI
@@ -26,6 +31,12 @@ reads and an actual SSE mutation, two identities, changes, forgery, multi-entry
payloads, whole replacement, cross-person removal, own-removal and moderation.
Run with Node 24+ and `PAGELIKE_BINARY=/absolute/path/to/pagelike`.
+Also run `node examples/vote/check-browser.mjs` with that variable and Playwright
+installed (or `PLAYWRIGHT_MODULE` pointing to its `index.mjs`). It verifies an
+existing choice appears before interaction, opening adds no vote, and a
+deliberate change updates the choice. It uses fixture identity with the real
+runtime; the platform's separate integration verifies restoration across logins.
+
These checks have a specific contract. For a new activity, adapt its checks and
prove them against deliberately defective variants before trusting green output.
Browser and Player login checks remain separate. Never point mutation tests at
diff --git a/directory/differ/skills/publish-content/examples/vote/check-browser.mjs b/directory/differ/skills/publish-content/examples/vote/check-browser.mjs
new file mode 100644
index 0000000..63074f2
--- /dev/null
+++ b/directory/differ/skills/publish-content/examples/vote/check-browser.mjs
@@ -0,0 +1,26 @@
+import assert from 'node:assert/strict';
+import { fileURLToPath } from 'node:url';
+import { readBundle, runtime } from '../../scripts/runtime.mjs';
+import { launchBrowser, viewer } from '../../scripts/browser.mjs';
+import { voteRequest } from './content/vote.mjs';
+
+const host = await runtime(); let browser;
+try {
+ browser = await launchBrowser();
+ const site = await host.site(await readBundle(fileURLToPath(new URL('./content/', import.meta.url))));
+ const alice = await site.participate('p_alice');
+ const saved = await site.request('/data/votes.html', voteRequest('p_alice', 'tea'), alice);
+ assert.equal(saved.status, 206); await saved.text();
+ const before = await (await site.request('/data/votes.html')).text();
+ const returning = await viewer(site, browser, 'p_alice'), guest = await viewer(site, browser, null);
+ try {
+ await returning.page.locator('[data-choice="tea"][aria-pressed="true"]').waitFor();
+ assert.equal(await returning.page.locator('#mine').textContent(), 'Your current choice: tea.');
+ assert.equal(await guest.page.locator('[aria-pressed="true"]').count(), 0);
+ assert.equal(await (await site.request('/data/votes.html')).text(), before, 'opening must not add a vote');
+ await returning.page.getByRole('button', { name: 'Coffee' }).click();
+ await returning.page.locator('[data-choice="coffee"][aria-pressed="true"]').waitFor();
+ assert.equal(await returning.page.locator('#totals').textContent(), 'Tea 0 · Coffee 1');
+ console.log('PASS: returning identity displays its existing vote before interaction; opening writes nothing; an intentional change updates it. Fixture identity, real runtime and browser.');
+ } finally { await returning.context.close(); await guest.context.close(); }
+} finally { if (browser) await browser.close(); await host.close(); }
diff --git a/directory/differ/skills/publish-content/examples/vote/content/app.mjs b/directory/differ/skills/publish-content/examples/vote/content/app.mjs
index 7a42fa7..20dcaad 100644
--- a/directory/differ/skills/publish-content/examples/vote/content/app.mjs
+++ b/directory/differ/skills/publish-content/examples/vote/content/app.mjs
@@ -1,6 +1,7 @@
import { voteRequest, currentVotes } from './vote.mjs';
const status = document.querySelector('#status');
let reading = false, dirty = false;
+let participant = null;
async function refresh() {
dirty = true;
if (reading) return;
@@ -10,8 +11,10 @@ async function refresh() {
dirty = false;
const response = await fetch('/data/votes.html', { cache: 'no-store' });
if (!response.ok) throw Error('The room could not be loaded. Reconnecting…');
- const votes = currentVotes(new DOMParser().parseFromString(await response.text(), 'text/html'));
+ const votes = currentVotes(new DOMParser().parseFromString(await response.text(), 'text/html'), participant);
document.querySelector('#totals').textContent = `Tea ${votes.tea} · Coffee ${votes.coffee}`;
+ document.querySelector('#mine').textContent = votes.mine ? `Your current choice: ${votes.mine}.` : '';
+ for (const button of document.querySelectorAll('[data-choice]')) button.setAttribute('aria-pressed', String(button.dataset.choice === votes.mine));
}
} catch (error) { status.textContent = error.message; }
finally { reading = false; }
@@ -26,7 +29,7 @@ for (const button of document.querySelectorAll('[data-choice]')) button.addEvent
const buttons = [...document.querySelectorAll('button')];
buttons.forEach(item => { item.disabled = true; });
try {
- const participant = await pagelike.participate();
+ participant = await pagelike.participate();
const response = await fetch('/data/votes.html', voteRequest(participant, button.dataset.choice));
if (!response.ok) throw Error(`Your choice was not saved (${response.status}). Please try again.`);
status.textContent = 'Your latest choice counts.';
@@ -35,3 +38,5 @@ for (const button of document.querySelectorAll('[data-choice]')) button.addEvent
finally { buttons.forEach(item => { item.disabled = false; }); }
});
await refresh();
+participant = await pagelike.identity();
+if (participant) await refresh();
diff --git a/directory/differ/skills/publish-content/examples/vote/content/index.html b/directory/differ/skills/publish-content/examples/vote/content/index.html
index d4bea73..b6220cd 100644
--- a/directory/differ/skills/publish-content/examples/vote/content/index.html
+++ b/directory/differ/skills/publish-content/examples/vote/content/index.html
@@ -4,6 +4,7 @@
The afternoon question
Tea or coffee?
+
One current choice per signed-in identity. Change your mind whenever you like. Your latest remaining choice counts. Use Player’s Your participation to remove your contributions.