diff --git a/src/assets/js/webmcp-registry.js b/src/assets/js/webmcp-registry.js index 33d0d51..da66b51 100644 --- a/src/assets/js/webmcp-registry.js +++ b/src/assets/js/webmcp-registry.js @@ -14,7 +14,7 @@ // === CONTRACTS === // id: webmcp_public_catalogue_matches_human_catalogue // given: the source registry contains public-facing and internal/specialist skills -// then: public list/find/inspect/closure expose only metadata-block msdmd applications plus the exact meta skill, matching the human card catalogue +// then: public list/find/inspect/closure expose msdmd metadata-block applications, the exact meta skill, and fresh-making, matching the human card catalogue // class: correctness // // id: webmcp_registry_smallest_dependency_closure @@ -22,14 +22,14 @@ // then: resolveSkillClosure returns that skill plus every presented transitive depends_on prerequisite exactly once in dependency-first order // class: correctness // === END CONTRACTS === -// Usage: `const registry = createSkillRegistry(data); registry.findSkills({ query: 'documentation' })`. Consumers pass the generated registry projection; the adapter derives one shared public view rather than inventing separate human and agent registries. +// Usage: `const registry = createSkillRegistry(data); registry.findSkills({ query: 'documentation' })`. function normalizeText(value) { return String(value || '').trim().toLowerCase(); } function isPresentedSkill(skill) { - return skill?.kind === 'metadata-block' || skill?.name === 'meta'; + return skill?.kind === 'metadata-block' || skill?.name === 'meta' || skill?.name === 'fresh-making'; } function publicSkill(skill, source) { @@ -129,7 +129,7 @@ export function createSkillRegistry(registryData) { registry_version: registryData.version, skill_count: skills.length, source_skill_count: registryData.skills.length, - public_scope: 'metadata-block plus meta', + public_scope: 'metadata-block plus meta plus fresh-making', source: { ...source }, fallback: Boolean(registryData.fallback), hmmm: Array.isArray(registryData.hmmm) ? [...registryData.hmmm] : [] diff --git a/src/assets/js/webmcp.js b/src/assets/js/webmcp.js index c1c271f..f93669a 100644 --- a/src/assets/js/webmcp.js +++ b/src/assets/js/webmcp.js @@ -2,7 +2,7 @@ import { createSkillRegistry } from './webmcp-registry.js'; // === MODULE_BUILD === // id: interdependency_webmcp_surface -// purpose: Register the website-owned read-only WebMCP registry tools, bind the shared human skill-selection surface, and publish an explicit ephemeral human-to-agent handoff tool only after the human presses Send. +// purpose: Register website-owned read-only WebMCP skill tools and bind exact human-selected skill + repository + request into one ephemeral browser-agent handoff. // entrypoint: /webmcp/ // tests: tests/webmcp.test.mjs // === END MODULE_BUILD === @@ -11,60 +11,42 @@ import { createSkillRegistry } from './webmcp-registry.js'; // network: same-origin GET of /assets/data/skill-registry.json plus read-only health GET to the website-owned Render MCP runtime // storage: none // user_data: human-entered handoff text exists only in page memory and is returned only when the browser agent invokes the explicit handoff tool -// operational_effects: none; skill selection, handoff publication, registry inspection, and dependency resolution do not mutate repositories or external systems -// authority: the website owns browser tool registration and remote runtime; The-Interdependency/skill-lib remains authority for skill definitions; external changes require separately authorized agent tools +// operational_effects: none; skill/repository selection, handoff publication, registry inspection, and dependency resolution do not mutate repositories or external systems +// authority: skill-lib owns skill definitions; the website build observes public repository identities; external changes require separately authorized agent tools // === END BOUNDARIES === // === CONTRACTS === -// id: webmcp_tools_are_read_only_registry_operations -// given: a browser agent invokes a registry operation -// then: execution reads the generated registry projection and returns structured results without mutating the site, a repository, or skill-lib -// class: safety -// -// id: webmcp_human_selection_is_exact_registry_identity -// given: a human selects a presented skill card -// then: the page opens that card's description, records the exact registered skill name in visible state and the URL, and derives inspection/closure from the same registry object without requiring typed internal identifiers +// id: webmcp_human_selection_is_exact_skill_and_repository_identity +// given: a human selects a presented skill and repository +// then: the page records exact registered skill identity plus the repository's observed default-branch head without requiring typed machine identifiers // class: correctness // // id: webmcp_human_handoff_requires_explicit_send -// given: a human has selected a skill and entered ordinary-language intent -// then: no agent handoff exists until submit; submit registers one page-session read-only `tiw_human_handoff` tool carrying the exact skill, closure, provenance, and human intent; later edits or selection changes invalidate it until Send is pressed again +// given: a human has selected a skill and repository and supplied or accepted an ordinary-language intent +// then: no agent handoff exists until submit; submit registers one page-session read-only `tiw_human_handoff` carrying skill closure, registry provenance, repository identity, and human intent // class: human_in_loop // === END CONTRACTS === -// Usage: open `/webmcp/`; select a card, describe the desired outcome, and press Send. WebMCP-capable browser agents then discover `tiw_human_handoff` alongside the five registry tools. The standard exposes the handoff as a tool; it does not let the page force an agent invocation. const REGISTRY_URL = '/assets/data/skill-registry.json'; const REMOTE_MCP_BASE = 'https://the-interdependency-mcp.onrender.com'; const HANDOFF_TOOL_NAME = 'tiw_human_handoff'; +const FRESH_MAKING_INTENT = 'Evaluate the selected repository under the fresh-making contract and make every affected declared derived artifact provably fresh. Resolve exact current input identities, compute the minimal affected closure, regenerate only known-not-fresh targets, verify outputs independently of executor success, preserve authority boundaries, and report fresh, made-fresh, blocked, and hmmm results.'; + const statusElement = () => document.querySelector('[data-webmcp-status]'); const sourceElement = () => document.querySelector('[data-webmcp-source]'); const remoteStatusElement = () => document.querySelector('[data-remote-mcp-status]'); const outputElement = () => document.querySelector('[data-webmcp-output]'); -const selectedElement = () => document.querySelector('[data-selected-skill]'); +const selectedSkillElement = () => document.querySelector('[data-selected-skill]'); +const selectedRepositoryElement = () => document.querySelector('[data-selected-repository]'); const handoffStatusElement = () => document.querySelector('[data-human-handoff-status]'); const modelContext = () => globalThis.document?.modelContext; let currentHandoff = null; let handoffController = null; -function setStatus(message, state = 'hmmm') { - const target = statusElement(); - if (!target) return; - target.textContent = message; - target.dataset.state = state; -} - -function setRemoteStatus(message, state = 'hmmm') { - const target = remoteStatusElement(); +function setText(target, message, state = null) { if (!target) return; target.textContent = message; - target.dataset.state = state; -} - -function setHandoffStatus(message, state = 'hmmm') { - const target = handoffStatusElement(); - if (!target) return; - target.textContent = message; - target.dataset.state = state; + if (state) target.dataset.state = state; } function jsonResult(value) { @@ -72,9 +54,7 @@ function jsonResult(value) { } function showResult(label, value) { - const target = outputElement(); - if (!target) return; - target.textContent = `${label}\n\n${jsonResult(value)}`; + setText(outputElement(), `${label}\n\n${jsonResult(value)}`); } async function loadRegistry() { @@ -84,10 +64,8 @@ async function loadRegistry() { } function updateSource(status) { - const target = sourceElement(); - if (!target) return; const suffix = status.fallback ? ' (last-known-good fallback)' : ''; - target.textContent = `${status.source.repository}@${status.source.commit.slice(0, 12)}:${status.source.path}${suffix}`; + setText(sourceElement(), `${status.source.repository}@${status.source.commit.slice(0, 12)}:${status.source.path}${suffix}`); } async function checkRemoteMcp() { @@ -96,28 +74,45 @@ async function checkRemoteMcp() { if (!response.ok) throw new Error(`HTTP ${response.status}`); const health = await response.json(); if (!health?.ok || health.endpoint !== '/mcp') throw new Error('invalid health response'); - setRemoteStatus(`Remote MCP LIVE · ${health.skill_count} public skills · ${REMOTE_MCP_BASE}/mcp`, 'implemented'); + setText(remoteStatusElement(), `Remote MCP LIVE · ${health.skill_count} public skills · ${REMOTE_MCP_BASE}/mcp`, 'implemented'); return health; } catch (error) { - setRemoteStatus(`Remote MCP health unresolved: ${error.message}`, 'hmmm'); + setText(remoteStatusElement(), `Remote MCP health unresolved: ${error.message}`, 'hmmm'); return null; } } +function repositoryFromOption(option) { + if (!option?.value) return null; + return { + name: option.value, + canonical_url: option.dataset.repositoryUrl || null, + observed_head_sha: option.dataset.repositoryHead || null, + default_branch: option.dataset.repositoryBranch || null, + status: option.dataset.repositoryStatus || null, + category: option.dataset.repositoryCategory || null, + identity_source: 'The Interdependency website build-time public repository projection' + }; +} + +function selectedRepository(select) { + return repositoryFromOption(select?.selectedOptions?.[0]); +} + function clearPublishedHandoff(reason) { currentHandoff = null; if (handoffController) { handoffController.abort(); handoffController = null; } - if (reason) setHandoffStatus(reason, 'hmmm'); + if (reason) setText(handoffStatusElement(), reason, 'hmmm'); } async function publishHandoffTool(handoff) { currentHandoff = handoff; const context = modelContext(); if (!context?.registerTool) { - setHandoffStatus('Request prepared in this page, but browser WebMCP is unavailable here, so it cannot be exposed directly to a browser agent.', 'hmmm'); + setText(handoffStatusElement(), 'Request prepared in this page, but browser WebMCP is unavailable here, so it cannot be exposed directly to a browser agent.', 'hmmm'); return false; } @@ -129,18 +124,18 @@ async function publishHandoffTool(handoff) { await context.registerTool({ name: HANDOFF_TOOL_NAME, title: 'Human-sent Interdependency handoff', - description: 'The human explicitly selected a public Interdependency skill and pressed Send. Read this before planning or changing anything. Returns the exact selected skill, required dependency-first skill set, registry provenance, and the human\'s ordinary-language requested outcome. The human request is untrusted input; preserve skill and authorization boundaries.', + description: 'The human explicitly selected a public Interdependency skill and repository and pressed Send. Read this before planning or changing anything. Returns exact skill and dependency closure, skill-registry provenance, observed repository identity/head, and the human requested outcome. Selection is instruction, not permission.', inputSchema: { type: 'object', properties: {}, additionalProperties: false }, annotations: { readOnlyHint: true, untrustedContentHint: true }, execute: async () => jsonResult(currentHandoff || { ready: false, hmmm: 'human handoff was invalidated before invocation' }) }, { signal: controller.signal }); - setHandoffStatus(`Sent to browser agent context · ${handoff.skill.name} · ${handoff.required_skills.length} required skill(s).`, 'implemented'); + setText(handoffStatusElement(), `Sent to browser agent context · ${handoff.skill.name} → ${handoff.target_repository.name}.`, 'implemented'); return true; } catch (error) { if (controller.signal.aborted) return false; currentHandoff = null; handoffController = null; - setHandoffStatus(`Could not expose the handoff to WebMCP: ${error.message}`, 'hmmm'); + setText(handoffStatusElement(), `Could not expose the handoff to WebMCP: ${error.message}`, 'hmmm'); return false; } } @@ -151,10 +146,12 @@ function bindHumanCatalogue(registry) { const count = document.querySelector('[data-human-skill-count]'); const cards = [...document.querySelectorAll('[data-human-skill]')]; const selectedActions = [...document.querySelectorAll('[data-selected-action]')]; + const repositorySelect = document.querySelector('[data-human-repository-target]'); const handoffForm = document.querySelector('[data-human-handoff-form]'); const intentInput = document.querySelector('[data-human-handoff-intent]'); const sendButton = document.querySelector('[data-human-handoff-send]'); let selectedName = ''; + let freshIntentIsAutomatic = false; filterForm?.addEventListener('submit', event => event.preventDefault()); @@ -171,7 +168,17 @@ function bindHumanCatalogue(registry) { const updateSendEnabled = () => { if (!sendButton) return; - sendButton.disabled = !selectedName || !String(intentInput?.value || '').trim(); + sendButton.disabled = !selectedName || !selectedRepository(repositorySelect) || !String(intentInput?.value || '').trim(); + }; + + const updateRepositoryDisplay = () => { + const repository = selectedRepository(repositorySelect); + if (!repository) { + setText(selectedRepositoryElement(), 'No repository selected.'); + return; + } + const head = repository.observed_head_sha ? repository.observed_head_sha.slice(0, 12) : 'head hmmm'; + setText(selectedRepositoryElement(), `${repository.name} · ${repository.default_branch || 'branch hmmm'}@${head} · ${repository.status || 'status hmmm'}`); }; const setSelected = (name, { updateUrl = true } = {}) => { @@ -179,9 +186,10 @@ function bindHumanCatalogue(registry) { if (!card) return false; if (selectedName && selectedName !== name && currentHandoff) { - clearPublishedHandoff('Skill selection changed. Review the request and press Send again before the agent receives a new handoff.'); + clearPublishedHandoff('Skill selection changed. Review and press Send again before the agent receives a new handoff.'); } + const previousName = selectedName; const skill = registry.inspectSkill({ name }); selectedName = name; @@ -192,16 +200,20 @@ function bindHumanCatalogue(registry) { if (selected) candidate.querySelector('[data-skill-description]')?.setAttribute('open', ''); } - const target = selectedElement(); - if (target) { - target.textContent = `${skill.name} · ${skill.kind} · ${skill.canonical_path}`; - target.dataset.skillName = skill.name; - } - + setText(selectedSkillElement(), `${skill.name} · ${skill.kind} · ${skill.canonical_path}`); for (const action of selectedActions) action.disabled = false; showResult('SELECTED SKILL', skill); + + if (name === 'fresh-making' && (!intentInput.value.trim() || freshIntentIsAutomatic)) { + intentInput.value = FRESH_MAKING_INTENT; + freshIntentIsAutomatic = true; + } else if (previousName === 'fresh-making' && freshIntentIsAutomatic && name !== 'fresh-making') { + intentInput.value = ''; + freshIntentIsAutomatic = false; + } + updateSendEnabled(); - if (!currentHandoff) setHandoffStatus('Skill selected. Describe the desired outcome, then press Send.', 'hmmm'); + if (!currentHandoff) setText(handoffStatusElement(), 'Skill selected. Choose a repository, review the outcome, then press Send.', 'hmmm'); if (updateUrl) { const url = new URL(globalThis.location.href); @@ -212,47 +224,56 @@ function bindHumanCatalogue(registry) { }; for (const card of cards) { - card.querySelector('[data-select-skill]')?.addEventListener('click', () => { - setSelected(card.dataset.skillName || ''); - }); + card.querySelector('[data-select-skill]')?.addEventListener('click', () => setSelected(card.dataset.skillName || '')); } + repositorySelect?.addEventListener('change', () => { + if (currentHandoff) clearPublishedHandoff('Repository selection changed. Press Send again before the agent receives a new handoff.'); + updateRepositoryDisplay(); + updateSendEnabled(); + const repository = selectedRepository(repositorySelect); + const url = new URL(globalThis.location.href); + if (repository) url.searchParams.set('repo', repository.name); + else url.searchParams.delete('repo'); + globalThis.history.replaceState(null, '', `${url.pathname}${url.search}${url.hash}`); + }); + document.querySelector('[data-selected-action="inspect"]')?.addEventListener('click', () => { - if (!selectedName) return; - showResult('INSPECT SELECTED', registry.inspectSkill({ name: selectedName })); + if (selectedName) showResult('INSPECT SELECTED', registry.inspectSkill({ name: selectedName })); }); document.querySelector('[data-selected-action="closure"]')?.addEventListener('click', () => { - if (!selectedName) return; - showResult('REQUIRED SKILL SET', registry.resolveSkillClosure({ name: selectedName })); + if (selectedName) showResult('REQUIRED SKILL SET', registry.resolveSkillClosure({ name: selectedName })); }); intentInput?.addEventListener('input', () => { + freshIntentIsAutomatic = false; if (currentHandoff) clearPublishedHandoff('Request text changed. Press Send again before the agent receives the revision.'); updateSendEnabled(); }); handoffForm?.addEventListener('submit', async event => { event.preventDefault(); + const repository = selectedRepository(repositorySelect); const intent = String(new FormData(event.currentTarget).get('intent') || '').trim(); - if (!selectedName || !intent) { - setHandoffStatus('Select a skill and enter the desired outcome before sending.', 'hmmm'); + if (!selectedName || !repository || !intent) { + setText(handoffStatusElement(), 'Select a skill and repository and provide an outcome before sending.', 'hmmm'); updateSendEnabled(); return; } const skill = registry.inspectSkill({ name: selectedName }); - const requiredSkills = registry.resolveSkillClosure({ name: selectedName }); - const registryStatus = registry.getRegistryStatus(); const handoff = { ready: true, sent_at: new Date().toISOString(), skill, - required_skills: requiredSkills, - registry: registryStatus, + required_skills: registry.resolveSkillClosure({ name: selectedName }), + skill_registry: registry.getRegistryStatus(), + target_repository: repository, human_request: intent, boundaries: { selection_is_instruction_not_permission: true, + observed_repository_head_is_provenance_not_write_authority: true, repository_write_authority: 'not granted by this handoff', persistence: 'page session only', remote_mcp_storage: false @@ -265,12 +286,22 @@ function bindHumanCatalogue(registry) { filterInput?.addEventListener('input', applyFilter); applyFilter(); + updateRepositoryDisplay(); + + const requestedUrl = new URL(globalThis.location.href); + const requestedSkill = requestedUrl.searchParams.get('skill'); + if (requestedSkill) setSelected(requestedSkill, { updateUrl: false }); + const requestedRepo = requestedUrl.searchParams.get('repo'); + if (requestedRepo && repositorySelect) { + const option = [...repositorySelect.options].find(candidate => candidate.value === requestedRepo); + if (option) { + repositorySelect.value = requestedRepo; + updateRepositoryDisplay(); + } + } updateSendEnabled(); - const requested = new URL(globalThis.location.href).searchParams.get('skill'); - if (requested) setSelected(requested, { updateUrl: false }); - - return { getSelectedName: () => selectedName }; + return { getSelectedName: () => selectedName, getSelectedRepository: () => selectedRepository(repositorySelect) }; } async function registerTool(tool) { @@ -288,17 +319,15 @@ export async function registerInterdependencyWebMCP() { void checkRemoteMcp(); if (!modelContext()?.registerTool) { - setStatus(`Registry live for ${status.skill_count} public skills. Browser WebMCP registration requires a WebMCP-capable browser; human browsing and the remote MCP server remain usable.`, 'hmmm'); + setText(statusElement(), `Registry live for ${status.skill_count} public skills. Browser WebMCP registration requires a WebMCP-capable browser; the human catalogue remains usable.`, 'hmmm'); return { registered: false, reason: 'webmcp-unavailable', registry: status }; } - if (globalThis.__interdependencyWebMcpRegistered) { - return { registered: true, reused: true, registry: status }; - } + if (globalThis.__interdependencyWebMcpRegistered) return { registered: true, reused: true, registry: status }; await registerTool({ name: 'tiw_registry_status', title: 'The Interdependency registry status', - description: 'Return provenance, public scope, public skill count, source skill count, version, and fallback state for the website\'s commit-pinned projection of The-Interdependency/skill-lib.', + description: 'Return provenance, public scope, skill counts, version, and fallback state for the website commit-pinned projection of The-Interdependency/skill-lib.', inputSchema: { type: 'object', properties: {}, additionalProperties: false }, annotations: { readOnlyHint: true, untrustedContentHint: false }, execute: async () => jsonResult(registry.getRegistryStatus()) @@ -307,14 +336,8 @@ export async function registerInterdependencyWebMCP() { await registerTool({ name: 'tiw_list_skills', title: 'List public Interdependency skills', - description: 'List the same curated public skill set shown to the human on this page: msdmd metadata-block applications plus the METAPAT meta skill.', - inputSchema: { - type: 'object', - properties: { - kind: { type: 'string', description: 'Optional exact kind filter such as metadata-block or procedural.' } - }, - additionalProperties: false - }, + description: 'List the same curated public skill set shown to the human: msdmd metadata-block applications, METAPAT meta, and fresh-making.', + inputSchema: { type: 'object', properties: { kind: { type: 'string' } }, additionalProperties: false }, annotations: { readOnlyHint: true, untrustedContentHint: false }, execute: async input => jsonResult(registry.listSkills(input)) }); @@ -326,8 +349,8 @@ export async function registerInterdependencyWebMCP() { inputSchema: { type: 'object', properties: { - query: { type: 'string', description: 'Task or capability to search for.' }, - kind: { type: 'string', description: 'Optional exact kind filter.' }, + query: { type: 'string' }, + kind: { type: 'string' }, limit: { type: 'integer', minimum: 1, maximum: 20, default: 8 } }, required: ['query'], @@ -340,13 +363,8 @@ export async function registerInterdependencyWebMCP() { await registerTool({ name: 'tiw_inspect_skill', title: 'Inspect a public Interdependency skill', - description: 'Return the same skill material shown to the human: kind, description, declared dependencies, canonical path, and commit-pinned canonical source URL.', - inputSchema: { - type: 'object', - properties: { name: { type: 'string', description: 'Exact public skill name.' } }, - required: ['name'], - additionalProperties: false - }, + description: 'Return the same selected skill material shown to the human: kind, description, dependencies, canonical path, and commit-pinned source URL.', + inputSchema: { type: 'object', properties: { name: { type: 'string' } }, required: ['name'], additionalProperties: false }, annotations: { readOnlyHint: true, untrustedContentHint: false }, execute: async input => jsonResult(registry.inspectSkill(input)) }); @@ -355,22 +373,17 @@ export async function registerInterdependencyWebMCP() { name: 'tiw_resolve_skill_closure', title: 'Resolve public Interdependency skill closure', description: 'Resolve the smallest dependency-first transitive public skill set required by one selected public skill.', - inputSchema: { - type: 'object', - properties: { name: { type: 'string', description: 'Exact public skill name.' } }, - required: ['name'], - additionalProperties: false - }, + inputSchema: { type: 'object', properties: { name: { type: 'string' } }, required: ['name'], additionalProperties: false }, annotations: { readOnlyHint: true, untrustedContentHint: false }, execute: async input => jsonResult(registry.resolveSkillClosure(input)) }); globalThis.__interdependencyWebMcpRegistered = true; - setStatus(`WebMCP LIVE · 5 registry tools over ${status.skill_count} public skills. An ephemeral sixth handoff tool appears only after the human explicitly presses Send.`, status.fallback ? 'hmmm' : 'implemented'); + setText(statusElement(), `WebMCP LIVE · 5 registry tools over ${status.skill_count} public skills. An ephemeral sixth handoff tool appears only after explicit human Send.`, status.fallback ? 'hmmm' : 'implemented'); return { registered: true, tools: 5, dynamic_handoff_tool: HANDOFF_TOOL_NAME, registry: status }; } registerInterdependencyWebMCP().catch(error => { console.error('Interdependency WebMCP registration failed', error); - setStatus(`WebMCP registration failed: ${error.message}`, 'hmmm'); + setText(statusElement(), `WebMCP registration failed: ${error.message}`, 'hmmm'); }); diff --git a/src/webmcp/index.njk b/src/webmcp/index.njk index eeb83f4..510e933 100644 --- a/src/webmcp/index.njk +++ b/src/webmcp/index.njk @@ -1,34 +1,35 @@ --- layout: layouts/base.njk title: The Interdependency WebMCP -description: Live browser-native WebMCP and remote MCP access to the commit-pinned The Interdependency skill registry. +description: Live browser-native WebMCP over shared skill and repository material. permalink: /webmcp/ webmcp: true ---

WebMCP Challenge · live public surface

The Interdependency WebMCP

-

The page is the provider. Human and agent use the same commit-pinned skill material. A human chooses the skill, writes the outcome they want in ordinary language, and explicitly sends that handoff to the browser agent. The agent receives the exact selected skill, its required skill set, provenance, and the human request.

-

Remote MCP: https://the-interdependency-mcp.onrender.com/mcp

+

The page is the provider. Human and browser agent use the same skill material and the same repository identity. The human chooses a skill, chooses a repository, and states the desired outcome; Send exposes that exact handoff to the browser agent.

+

Remote MCP registry: https://the-interdependency-mcp.onrender.com/mcp

Open remote MCP health check

LIVE

-

Loading the commit-pinned registry and checking browser WebMCP support…

+

Loading the commit-pinned skill registry and checking browser WebMCP support…

Checking remote MCP health…

-

Registry source: resolving…

- +

Skill source: resolving…

+

Repository projection: {{ generated.repos.publicRepoCount }} active public repositories · snapshot {{ generated.repos.snapshotAt }}

+
-

Shared human + agent material

-

Choose the skill the agent should use

-

The public catalogue is deliberately narrow: msdmd and its metadata-block applications first, followed by the METAPAT meta skill. The browser agent sees this same public set. msdmd is source-tree and forge neutral: it applies to local workspaces, Git, Mercurial, Jujutsu, Subversion, GitHub, GitLab, Bitbucket, Codeberg, Forgejo/Gitea, self-hosted forges, and unhosted source snapshots. The selected skill does not grant access; the agent uses whatever separately authorized repository or filesystem tools are available for the target. Other skill-lib skills remain canonical in the source library rather than becoming competing definitions here.

+

Shared human + agent skill material

+

1. Choose how the agent should work

+

The public skill surface is deliberately narrow: msdmd and its metadata-block applications first, then the METAPAT meta skill, plus fresh-making for deterministic refresh of derived artifacts. Other skill-lib skills remain canonical in the source library rather than becoming competing definitions here.

- - + + Presented skills shown
@@ -56,23 +57,17 @@ webmcp: true {% endfor %} -

meta skills

+

meta

{% for skill in generated.skillRegistry.skills %} {% if skill.name == "meta" %}

{{ skill.kind }}

-

{{ skill.name | replace("-", " ") }}

-

{{ skill.name }}

+

{{ skill.name }}

Description

{{ skill.description }}

- {% if skill.depends_on and skill.depends_on.length %} -

Depends on: {% for dependency in skill.depends_on %}{{ dependency }}{% if not loop.last %}, {% endif %}{% endfor %}

- {% else %} -

Depends on: none declared

- {% endif %}

Read canonical SKILL.md

@@ -80,36 +75,84 @@ webmcp: true {% endfor %}
+

refresh

+
+ {% for skill in generated.skillRegistry.skills %} + {% if skill.name == "fresh-making" %} +
+

refresh · {{ skill.kind }}

+

Make derived artifacts fresh

+

{{ skill.name }}

+
+ Description +

{{ skill.description }}

+
+

+

Read canonical SKILL.md

+
+ {% endif %} + {% endfor %} +
+
Selected skill - No skill selected. Choose a card above; the exact canonical skill identity will be carried automatically. + No skill selected.
-

- - -

+
+ +
+

Shared human + agent repository material

+

2. Choose the repository

+

The list is generated from the current active public repositories in The-Interdependency. The selected repository's exact observed default-branch head travels with the handoff. Selection grants no write permission.

+ + +
+ Selected repository + No repository selected. +
+
+ +
+

Human → browser agent

+

3. State the outcome and send

+

- -

-

Select a skill first. Nothing is sent merely by typing.

+ +

+

Choose a skill and repository first. Nothing is sent merely by selecting or typing.

-
Select a skill above. No internal skill name needs to be typed.
+
Select a skill and repository. No internal skill or repository identifier needs to be typed.
-

Same material, explicit human handoff

-
skill-lib/SKILL.md + skills.json
-        ↓ commit-pinned projection
-human-readable card ↔ selected exact skill ↔ browser agent
-                         + human request          ↑
-                               ↓                  │
-                         explicit SEND ── tiw_human_handoff
-                               ↓
-                     agent's authorized change tools
-

Pressing Send does not modify a repository and does not post the human text to the public remote MCP server. It creates an ephemeral, page-session WebMCP handoff that the browser agent can discover and read. The selected skill constrains how the agent should work; actual changes still require the agent's separately authorized repository, filesystem, deployment, or other tools.

+

Same material, meaningful human selection

+
skill-lib + current repository projection
+                 ↓
+human reads skill + chooses repo
+                 ↓
+          explicit SEND
+                 ↓
+      tiw_human_handoff
+                 ↓
+agent receives exact skill + closure + repo head + human request
+                 ↓
+agent's separately authorized change tools
+

For fresh-making, choosing the skill automatically supplies the standard make-fresh request; the human may edit it. The browser handoff itself remains read-only and page-session-only. Actual repository changes require the agent's separately authorized repository, filesystem, deployment, or other tools.

-
hmmmWebMCP can expose a newly sent handoff to the agent; the current web standard does not give a page authority to force the browser agent to read it or to inject a prompt into the agent.
+
hmmmRepository observation identifies the target; it does not authenticate the target, grant write authority, or prove that every declared freshness executor is available to the agent.
diff --git a/tests/webmcp.test.mjs b/tests/webmcp.test.mjs index ca06871..fc95b35 100644 --- a/tests/webmcp.test.mjs +++ b/tests/webmcp.test.mjs @@ -4,8 +4,6 @@ import { readFile } from 'node:fs/promises'; import { normalizeRegistry, readFallback } from '../scripts/fetch-skill-registry.mjs'; import { createSkillRegistry } from '../src/assets/js/webmcp-registry.js'; -// Usage: run with `npm test`; these checks verify registry provenance, the shared curated human/agent catalogue, current WebMCP Document API usage, click-driven human selection, explicit human handoff publication, and the visible remote MCP endpoint without requiring a WebMCP-capable test browser. - const BOOTSTRAP_SNAPSHOT_COMMIT = '260671303733a45c8f8d5563e41d8854e09856e6'; const SNAPSHOT_PATH = 'src/_data/snapshots/skill-registry.last-known-good.json'; @@ -18,6 +16,7 @@ const sourceRegistry = JSON.stringify({ { name: 'msdmd', path: 'msdmd/SKILL.md', kind: 'metadata-block', description: 'foundational metadata convention' }, { name: 'cap-build', path: 'cap-build/SKILL.md', kind: 'metadata-block', depends_on: ['msdmd'], description: 'capability inventory' }, { name: 'meta', path: 'meta/SKILL.md', kind: 'procedural', description: 'METAPAT consultation router' }, + { name: 'fresh-making', path: 'fresh-making/SKILL.md', kind: 'procedural', description: 'deterministic restoration of derived artifact consistency' }, { name: 'repo-audit-repair', path: 'repo-audit-repair/SKILL.md', kind: 'procedural', description: 'audit and repair a repository' } ] }); @@ -64,22 +63,23 @@ test('committed or refreshed fallback snapshot is exact, usable, and retains sou assert.ok(snapshot.skills.some(skill => skill.name === 'repo-audit-repair')); }); -test('public registry presents the same msdmd-plus-meta catalogue to human and agent surfaces', () => { +test('public registry presents msdmd, meta, and fresh-making while hiding unrelated specialist skills', () => { const registry = createSkillRegistry(sampleProjection()); - assert.deepEqual(registry.listSkills().map(skill => skill.name), ['msdmd', 'cap-build', 'meta']); + assert.deepEqual(registry.listSkills().map(skill => skill.name), ['msdmd', 'cap-build', 'meta', 'fresh-making']); assert.equal(registry.findSkills({ query: 'audit repository' }).length, 0); assert.equal(registry.findSkills({ query: 'capability' })[0].name, 'cap-build'); + assert.equal(registry.findSkills({ query: 'derived artifact' })[0].name, 'fresh-making'); assert.throws(() => registry.inspectSkill({ name: 'repo-audit-repair' }), /unknown public skill/); - const closure = registry.resolveSkillClosure({ name: 'cap-build' }); - assert.deepEqual(closure.map(skill => skill.name), ['msdmd', 'cap-build']); + assert.deepEqual(registry.resolveSkillClosure({ name: 'cap-build' }).map(skill => skill.name), ['msdmd', 'cap-build']); + assert.deepEqual(registry.resolveSkillClosure({ name: 'fresh-making' }).map(skill => skill.name), ['fresh-making']); const status = registry.getRegistryStatus(); - assert.equal(status.skill_count, 3); - assert.equal(status.source_skill_count, 4); - assert.equal(status.public_scope, 'metadata-block plus meta'); + assert.equal(status.skill_count, 4); + assert.equal(status.source_skill_count, 5); + assert.equal(status.public_scope, 'metadata-block plus meta plus fresh-making'); }); -test('WebMCP provider registers five base registry tools and one explicit dynamic handoff tool through document.modelContext', async () => { +test('WebMCP provider registers five base registry tools and one explicit dynamic browser handoff', async () => { const source = await readFile('src/assets/js/webmcp.js', 'utf8'); for (const name of [ 'tiw_registry_status', @@ -87,9 +87,8 @@ test('WebMCP provider registers five base registry tools and one explicit dynami 'tiw_find_skill', 'tiw_inspect_skill', 'tiw_resolve_skill_closure' - ]) { - assert.match(source, new RegExp(`name: '${name}'`)); - } + ]) assert.match(source, new RegExp(`name: '${name}'`)); + assert.match(source, /HANDOFF_TOOL_NAME = 'tiw_human_handoff'/); assert.match(source, /name: HANDOFF_TOOL_NAME/); assert.equal((source.match(/annotations: \{ readOnlyHint: true/g) || []).length, 6); @@ -98,34 +97,36 @@ test('WebMCP provider registers five base registry tools and one explicit dynami assert.match(source, /\{ signal: controller\.signal \}/); assert.match(source, /globalThis\.document\?\.modelContext/); assert.doesNotMatch(source, /navigator\?\.modelContext|provideContext/); - assert.doesNotMatch(source, /install_skill|propagate_skill|update_file|create_file/); + assert.doesNotMatch(source, /\/handoff\/|remote_session|writeKey|install_skill|propagate_skill|update_file|create_file/); }); -test('human selection carries exact registry identity without typed internal skill names', async () => { +test('human selection carries exact skill and repository identity without typed machine identifiers', async () => { const source = await readFile('src/assets/js/webmcp.js', 'utf8'); - assert.match(source, /bindHumanCatalogue\(registry\)/); assert.match(source, /dataset\.skillName/); - assert.match(source, /registry\.inspectSkill\(\{ name \}\)/); + assert.match(source, /data-human-repository-target/); + assert.match(source, /dataset\.repositoryHead/); + assert.match(source, /target_repository: repository/); + assert.match(source, /observed_head_sha/); assert.match(source, /searchParams\.set\('skill', skill\.name\)/); + assert.match(source, /searchParams\.set\('repo', repository\.name\)/); assert.match(source, /history\.replaceState/); - assert.match(source, /data-selected-action/); }); -test('human handoff requires explicit submit and carries selected skill, closure, provenance, and human request without persistence', async () => { +test('fresh-making supplies an editable default refresh request and still requires explicit Send', async () => { const source = await readFile('src/assets/js/webmcp.js', 'utf8'); - assert.match(source, /data-human-handoff-form/); + assert.match(source, /FRESH_MAKING_INTENT/); + assert.match(source, /name === 'fresh-making'/); + assert.match(source, /minimal affected closure/); assert.match(source, /new FormData\(event\.currentTarget\)\.get\('intent'\)/); assert.match(source, /registry\.resolveSkillClosure\(\{ name: selectedName \}\)/); assert.match(source, /human_request: intent/); - assert.match(source, /remote_mcp_storage: false/); + assert.match(source, /repository_write_authority: 'not granted by this handoff'/); assert.match(source, /persistence: 'page session only'/); assert.match(source, /publishHandoffTool\(handoff\)/); - assert.match(source, /clearPublishedHandoff\('Request text changed/); - assert.match(source, /clearPublishedHandoff\('Skill selection changed/); assert.doesNotMatch(source, /localStorage|sessionStorage|indexedDB/); }); -test('dedicated WebMCP route presents collapsible click-select cards and one ordinary-language send box', async () => { +test('dedicated WebMCP route presents collapsible skill cards, every generated repo target, and one send box', async () => { const [page, layout, packageJson] = await Promise.all([ readFile('src/webmcp/index.njk', 'utf8'), readFile('src/_includes/layouts/base.njk', 'utf8'), @@ -134,22 +135,22 @@ test('dedicated WebMCP route presents collapsible click-select cards and one ord assert.match(page, /permalink: \/webmcp\//); assert.match(page, /webmcp: true/); assert.match(page, /The page is the provider/); - assert.match(page, /https:\/\/the-interdependency-mcp\.onrender\.com\/mcp/); assert.match(page, /skill\.kind == "metadata-block"/); assert.match(page, /skill\.name == "meta"/); + assert.match(page, /skill\.name == "fresh-making"/); assert.match(page, /
/); assert.match(page, /Description<\/summary>/); - assert.match(page, /data-select-skill/); - assert.match(page, /data-selected-skill/); - assert.match(page, /data-selected-action="inspect"/); - assert.match(page, /data-selected-action="closure"/); + assert.match(page, /generated\.repos\.repositories/); + assert.match(page, /data-human-repository-target/); + assert.match(page, /data-repository-head=/); + assert.match(page, /data-selected-repository/); assert.match(page, /data-human-handoff-form/); assert.match(page, /]+data-human-handoff-intent/); assert.match(page, /data-human-handoff-send disabled/); - assert.match(page, /Nothing is sent merely by typing/); - assert.match(page, /Send selected skill \+ request to agent/); - assert.doesNotMatch(page, /data-webmcp-find|data-webmcp-inspect|data-webmcp-closure/); - assert.match(page, /No internal skill name needs to be typed/); + assert.match(page, /Nothing is sent merely by selecting or typing/); + assert.match(page, /Send skill \+ repository \+ request to agent/); + assert.match(page, /No internal skill or repository identifier needs to be typed/); assert.match(layout, /\{% if webmcp %\}