diff --git a/.gitattributes b/.gitattributes
index 8085800..342df04 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -14,3 +14,8 @@ manifests.json text eol=lf
netlify.toml text eol=lf
_headers text eol=lf
_redirects text eol=lf
+
+# Manifest mirrors are byte-exact copies of upstream responses; any eol
+# normalization would make the checkout differ from the fetched bytes and
+# _refresh_mirrors would see phantom changes on Windows checkouts.
+mirrors/** -text
diff --git a/.github/workflows/federation-sync.yml b/.github/workflows/federation-sync.yml
index acda435..69b0088 100644
--- a/.github/workflows/federation-sync.yml
+++ b/.github/workflows/federation-sync.yml
@@ -1,9 +1,15 @@
name: federation-sync
# Fetches every publisher's .well-known/install-manifests.json, merges
-# into manifests.json, and opens a PR with the diff (if any). The PR is
-# created with the `auto-sync-federation` label and `--auto --squash`
-# enabled, so it merges itself the moment required checks go green.
+# into manifests.json, refreshes the committed manifest mirrors
+# (mirrors/manifests/), rebuilds registry/ pages from those mirrors, and
+# opens a PR with the combined diff (if any). The PR is created with the
+# `auto-sync-federation` label and `--auto --squash` enabled, so it
+# merges itself the moment required checks go green.
+#
+# Manifest content, its mirror, and the rebuilt pages land in ONE
+# commit — syncing manifests without rebuilding pages is what kept
+# redding the registry drift check (PRs #54/#55/#58).
#
# Scheduled daily; can also be triggered manually via the Actions UI.
@@ -41,10 +47,14 @@ jobs:
- name: Run federation sync (live fetch)
run: python scripts/sync_from_publishers.py --allow-network
+ - name: Refresh manifest mirrors + rebuild registry pages
+ run: python scripts/build_registry_pages.py --refresh
+
- name: Check for diff
id: diff
run: |
- if git diff --quiet manifests.json; then
+ # git status (not diff) so brand-new mirror/page files count.
+ if [ -z "$(git status --porcelain -- manifests.json mirrors registry)" ]; then
echo "changed=false" >> "$GITHUB_OUTPUT"
else
echo "changed=true" >> "$GITHUB_OUTPUT"
@@ -69,12 +79,12 @@ jobs:
git config user.name "yep-federation-bot"
git config user.email "yep-federation-bot@users.noreply.github.com"
git checkout -b "$BRANCH"
- git add manifests.json
+ git add manifests.json mirrors registry
git commit -m "auto: federation sync $(date -u +%Y-%m-%d)"
git push origin "$BRANCH"
PR_URL=$(gh pr create \
--title "auto: federation sync $(date -u +%Y-%m-%d)" \
- --body "Automated federation pull. Diff is the result of fetching every publisher in publishers.json and re-rendering manifests.json. Merging this updates the public registry. See federation-sync workflow for details." \
+ --body "Automated federation pull. Diff is the result of fetching every publisher in publishers.json, re-rendering manifests.json, refreshing the committed manifest mirrors, and rebuilding registry pages from them. Merging this updates the public registry. See federation-sync workflow for details." \
--label "auto-sync-federation" \
--base main \
--head "$BRANCH")
diff --git a/mirrors/manifests/deep-research.json b/mirrors/manifests/deep-research.json
new file mode 100644
index 0000000..e3352f7
--- /dev/null
+++ b/mirrors/manifests/deep-research.json
@@ -0,0 +1,284 @@
+{
+ "manifest_version": "0.4",
+ "tool": {
+ "namespace": "drknowhow",
+ "id": "deep-research",
+ "version": "0.2.0",
+ "name": "deep_research",
+ "summary": "Protocol-first, gated, multi-agent literature investigation with no-fabrication enforced via verbatim quote spans.",
+ "description": "deep_research is an agent-runtime-agnostic workflow that turns an empirical research question into a citation-grade synthesis. Two human-in-the-loop gates (protocol pre-registration, Pass-2 spend) bracket a four-role subagent crew (Scout / Skeptic / Methodologist / Synthesizer). Every claim a synthesis ships must be backed by a row in the research_evidence table with a verbatim quote_span; claims without their quote get cut. v0.2.0 adds the Python reference implementation: a stdlib-urllib scholar adapter (OpenAlex / Semantic Scholar / PubMed / arXiv / Europe PMC / Crossref / Unpaywall) and a python-docx + matplotlib synthesis builder with pluggable upload. Core stays stdlib-only; the docx builder is gated behind the optional `[viz]` extra.",
+ "homepage": "https://github.com/drknowhow/deep-research",
+ "author": {
+ "name": "Dimitri T",
+ "url": "https://yepgent.com"
+ },
+ "license": "Apache-2.0",
+ "tags": [
+ "research",
+ "literature-review",
+ "meta-analysis",
+ "agents",
+ "subagent-fan-out",
+ "no-fabrication",
+ "skill-bundle"
+ ]
+ },
+ "runtime": {
+ "kind": "python-module",
+ "install": {
+ "method": "git",
+ "url": "https://github.com/drknowhow/deep-research",
+ "ref": "v0.2.0",
+ "layout": "skill-bundle"
+ },
+ "entrypoint": {
+ "command": ["python", "-m", "deep_research"]
+ }
+ },
+ "scopes": [
+ {
+ "resource": "net.outbound.scholarly",
+ "actions": ["read"],
+ "rationale": "Calls public scholarly APIs (OpenAlex, Semantic Scholar, PubMed, arXiv, Europe PMC, Crossref, Unpaywall) and fetches open-access PDFs to extract verbatim quote spans."
+ },
+ {
+ "resource": "db.research_tables",
+ "actions": ["read", "write"],
+ "rationale": "Persists the project row, every query (research_searches), and every claim with provenance (research_evidence). These three append-only tables are the audit trail."
+ }
+ ],
+ "actions": [
+ {
+ "name": "start_project",
+ "summary": "Create a new research project row in 'planned' status with the user-supplied question.",
+ "docs": {
+ "goal": "Open a new research investigation.",
+ "inputs_brief": "name (slug), title, question",
+ "outputs_brief": "research_project_id (uuid)",
+ "errors_brief": "schema_missing, db_unreachable",
+ "example": "start_project(name='cholesterol-acm', title='Primary-prevention LDL lowering and all-cause mortality', question='Does pharmacological LDL lowering reduce ACM in strict primary prevention?')"
+ },
+ "invocation": {
+ "kind": "stdin-json"
+ },
+ "input": {
+ "type": "object",
+ "required": ["name", "title", "question"],
+ "properties": {
+ "name": {"type": "string", "pattern": "^[a-z0-9-]+$"},
+ "title": {"type": "string", "minLength": 1},
+ "question": {"type": "string", "minLength": 1}
+ }
+ },
+ "output": {
+ "format": "json",
+ "schema": {
+ "type": "object",
+ "required": ["research_project_id", "status"],
+ "properties": {
+ "research_project_id": {"type": "string", "format": "uuid"},
+ "status": {"type": "string", "const": "planned"}
+ }
+ }
+ },
+ "side_effects": "write",
+ "idempotent": false,
+ "scopes_used": ["db.research_tables"],
+ "error_envelope": "standard"
+ },
+ {
+ "name": "submit_protocol",
+ "summary": "Attach the pre-registered protocol JSON to a project and move status to protocol_gated.",
+ "docs": {
+ "goal": "Lock the search/inclusion/effect/analysis plan before any paid searches.",
+ "inputs_brief": "research_project_id, protocol (PICO + queries + inclusion/exclusion + analysis_plan + gate_thresholds)",
+ "outputs_brief": "status='protocol_gated', awaiting_human_approval=true",
+ "errors_brief": "project_not_found, protocol_invalid",
+ "example": "submit_protocol(research_project_id=..., protocol={...})"
+ },
+ "invocation": {"kind": "stdin-json"},
+ "input": {
+ "type": "object",
+ "required": ["research_project_id", "protocol"],
+ "properties": {
+ "research_project_id": {"type": "string", "format": "uuid"},
+ "protocol": {"type": "object"}
+ }
+ },
+ "output": {
+ "format": "json",
+ "schema": {
+ "type": "object",
+ "required": ["status", "awaiting_human_approval"],
+ "properties": {
+ "status": {"type": "string"},
+ "awaiting_human_approval": {"type": "boolean"}
+ }
+ }
+ },
+ "side_effects": "write",
+ "idempotent": true,
+ "scopes_used": ["db.research_tables"],
+ "error_envelope": "standard"
+ },
+ {
+ "name": "approve_gate",
+ "summary": "Append a human-decision row to gate_log and transition the project to the next phase.",
+ "docs": {
+ "goal": "Record a Gate 1 or Gate 2 decision and advance state.",
+ "inputs_brief": "research_project_id, gate ('protocol'|'pass2'), decision ('approve'|'revise'|'abort'), notes",
+ "outputs_brief": "new status",
+ "errors_brief": "project_not_found, wrong_phase, unknown_gate",
+ "example": "approve_gate(research_project_id=..., gate='protocol', decision='approve')"
+ },
+ "invocation": {"kind": "stdin-json"},
+ "input": {
+ "type": "object",
+ "required": ["research_project_id", "gate", "decision"],
+ "properties": {
+ "research_project_id": {"type": "string", "format": "uuid"},
+ "gate": {"type": "string", "enum": ["protocol", "pass2"]},
+ "decision": {"type": "string", "enum": ["approve", "revise", "abort"]},
+ "notes": {"type": "string"}
+ }
+ },
+ "output": {
+ "format": "json",
+ "schema": {
+ "type": "object",
+ "required": ["status"],
+ "properties": {"status": {"type": "string"}}
+ }
+ },
+ "side_effects": "write",
+ "idempotent": false,
+ "scopes_used": ["db.research_tables"],
+ "error_envelope": "standard"
+ },
+ {
+ "name": "run_pass1",
+ "summary": "Fan out Scout / Skeptic / Methodologist subagents over the approved protocol. Writes research_searches and research_evidence rows.",
+ "docs": {
+ "goal": "Build the Pass-1 candidate corpus from abstracts.",
+ "inputs_brief": "research_project_id",
+ "outputs_brief": "scout_run, skeptic_run, methodologist_run summaries; corpus rollup",
+ "errors_brief": "wrong_phase, scholarly_api_unreachable, subagent_cap_exceeded",
+ "example": "run_pass1(research_project_id=...)"
+ },
+ "invocation": {"kind": "stdin-json"},
+ "input": {
+ "type": "object",
+ "required": ["research_project_id"],
+ "properties": {
+ "research_project_id": {"type": "string", "format": "uuid"}
+ }
+ },
+ "output": {
+ "format": "json",
+ "schema": {
+ "type": "object",
+ "required": ["scout", "skeptic", "methodologist", "rollup"],
+ "properties": {
+ "scout": {"type": "object"},
+ "skeptic": {"type": "object"},
+ "methodologist": {"type": "object"},
+ "rollup": {"type": "object"}
+ }
+ }
+ },
+ "side_effects": "write",
+ "idempotent": false,
+ "scopes_used": ["net.outbound.scholarly", "db.research_tables"],
+ "error_envelope": "standard"
+ },
+ {
+ "name": "run_pass2",
+ "summary": "Retrieve full text for the approved candidate set and stage it for the Synthesizer.",
+ "docs": {
+ "goal": "Pull OA PDFs and extract to text.",
+ "inputs_brief": "research_project_id",
+ "outputs_brief": "n_retrieved, n_paywalled, n_unavailable, candidate ids",
+ "errors_brief": "wrong_phase, pdf_extract_failed",
+ "example": "run_pass2(research_project_id=...)"
+ },
+ "invocation": {"kind": "stdin-json"},
+ "input": {
+ "type": "object",
+ "required": ["research_project_id"],
+ "properties": {
+ "research_project_id": {"type": "string", "format": "uuid"}
+ }
+ },
+ "output": {
+ "format": "json",
+ "schema": {
+ "type": "object",
+ "properties": {
+ "n_retrieved": {"type": "integer"},
+ "n_paywalled": {"type": "integer"},
+ "n_unavailable": {"type": "integer"}
+ }
+ }
+ },
+ "side_effects": "write",
+ "idempotent": false,
+ "scopes_used": ["net.outbound.scholarly", "db.research_tables"],
+ "error_envelope": "standard"
+ },
+ {
+ "name": "synthesize",
+ "summary": "Run the Synthesizer subagent. Produces a structured document; enforces verbatim-quote-or-cut on every claim.",
+ "docs": {
+ "goal": "Produce the no-fabrication synthesis document.",
+ "inputs_brief": "research_project_id",
+ "outputs_brief": "doc_artifact_id, n_cited, n_supports, n_refutes, n_mixed, complete",
+ "errors_brief": "wrong_phase, fabrication_detected, doc_build_failed",
+ "example": "synthesize(research_project_id=...)"
+ },
+ "invocation": {"kind": "stdin-json"},
+ "input": {
+ "type": "object",
+ "required": ["research_project_id"],
+ "properties": {
+ "research_project_id": {"type": "string", "format": "uuid"}
+ }
+ },
+ "output": {
+ "format": "json",
+ "schema": {
+ "type": "object",
+ "required": ["doc_artifact_id", "n_cited", "complete"],
+ "properties": {
+ "doc_artifact_id": {"type": "string"},
+ "n_cited": {"type": "integer"},
+ "n_supports": {"type": "integer"},
+ "n_refutes": {"type": "integer"},
+ "n_mixed": {"type": "integer"},
+ "complete": {"type": "boolean"}
+ }
+ }
+ },
+ "side_effects": "write",
+ "idempotent": false,
+ "scopes_used": ["db.research_tables"],
+ "error_envelope": "standard"
+ }
+ ],
+ "smoke": {
+ "kind": "shell",
+ "command": ["python", "-c", "import deep_research; print(deep_research.__version__)"],
+ "timeout_seconds": 15,
+ "success": {
+ "exit_code": 0,
+ "stdout_regex": "^0\\."
+ }
+ },
+ "kill_switch": {
+ "kind": "manual",
+ "instructions": "deep_research holds no credentials and persists nothing outside the research_* tables the host application provisions. To revoke: (1) drop or revoke INSERT/UPDATE/DELETE on research_projects, research_searches, research_evidence in your application's database; (2) remove the deep_research module from your agent's runtime PYTHONPATH or skill registry. No vendor account to close."
+ },
+ "support": {
+ "issues_url": "https://github.com/drknowhow/deep-research/issues",
+ "docs_url": "https://github.com/drknowhow/deep-research#readme"
+ }
+}
diff --git a/mirrors/manifests/muninn-blog-publish.json b/mirrors/manifests/muninn-blog-publish.json
new file mode 100644
index 0000000..f00d37e
--- /dev/null
+++ b/mirrors/manifests/muninn-blog-publish.json
@@ -0,0 +1,299 @@
+{
+ "manifest_version": "0.4",
+ "tool": {
+ "id": "muninn-blog-publish",
+ "version": "0.1.0",
+ "name": "Muninn blog_publish",
+ "summary": "Publish HTML pages to austegard.com via GitHub Pages, optionally update the Atom feed, optionally announce on Bluesky with a follow-up engagement-link commit. Encoded as a flowing DAG so the bsky chain is detached and partial-failure-tolerant.",
+ "description": "Encoded as a flowing graph: page-commit \u2192 wait-for-deploy \u2192 (when feed_entry given) feed-update; bsky-announce + engagement-link-commit run as detached side-effects. Caller gets the page URL the moment GH Pages serves it; bsky failures land in `flow.detached_failures` and never bubble up as publish failures. The 300-grapheme bsky cap is enforced as a `validate=` gate BEFORE any createRecord call. Same primary credential as `perch_publish` (GH_TOKEN), same secondary credentials as `bsky_card` (MUNINN_BSKY_HANDLE, MUNINN_BSKY_APP_PASSWORD) \u2014 third publishing target after perch_publish and (planned) whtwnd. Issue #5 calls this out as the test for whether v0.4 `writes[]` generalises across multiple publishing surfaces in the same agent.",
+ "homepage": "https://github.com/oaustegard/muninn-utilities/blob/main/muninn_utils/blog_publish.py",
+ "author": {
+ "name": "Muninn (raven of memory; agent operating on behalf of Oskar Austegard)",
+ "url": "https://muninn.austegard.com"
+ },
+ "license": "MIT",
+ "tags": [
+ "publishing",
+ "github-pages",
+ "atom-feed",
+ "blog",
+ "bluesky",
+ "flowing"
+ ]
+ },
+ "runtime": {
+ "kind": "python-module",
+ "install": {
+ "method": "preinstalled",
+ "locator": {
+ "kind": "python-module",
+ "module": "muninn_utils.blog_publish"
+ }
+ },
+ "entrypoint": {
+ "command": [
+ "python",
+ "-m",
+ "muninn_utils.blog_publish"
+ ]
+ }
+ },
+ "env": [
+ {
+ "name": "GH_TOKEN",
+ "prompt": "GitHub personal access token. Needs write access to the publish-target repo (oaustegard/austegard.com by default). Same coarse credential as perch_publish, issue_close, perch_triage, verify_patch \u2014 share-by-default is intentional. Falls back to GITHUB_TOKEN if GH_TOKEN is not set.",
+ "secret": true,
+ "required": true,
+ "validation_regex": "^(ghp_[A-Za-z0-9]{36}|github_pat_[A-Za-z0-9_]+)$",
+ "obtain_url": "https://github.com/settings/personal-access-tokens"
+ },
+ {
+ "name": "GITHUB_TOKEN",
+ "prompt": "Optional fallback for GH_TOKEN. The source reads `os.environ.get('GH_TOKEN') or os.environ.get('GITHUB_TOKEN')`, so either name works. Same scope/sensitivity as GH_TOKEN.",
+ "secret": true,
+ "required": false,
+ "validation_regex": "^(ghp_[A-Za-z0-9]{36}|github_pat_[A-Za-z0-9_]+)$",
+ "obtain_url": "https://github.com/settings/personal-access-tokens"
+ },
+ {
+ "name": "MUNINN_BSKY_HANDLE",
+ "prompt": "The Bluesky handle to post the announcement as, e.g. 'austegard.com'. Optional \u2014 leave blank to skip the bsky chain entirely (the page-publish path still runs).",
+ "secret": false,
+ "required": false,
+ "validation_regex": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$",
+ "obtain_url": "https://bsky.app/settings/account"
+ },
+ {
+ "name": "MUNINN_BSKY_APP_PASSWORD",
+ "prompt": "App password for the Bluesky handle above. Format is four hyphen-separated four-character groups. Treat as a secret. Required only if MUNINN_BSKY_HANDLE is set; otherwise leave blank. Bsky app passwords cannot be programmatically revoked.",
+ "secret": true,
+ "required": false,
+ "validation_regex": "^[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}$",
+ "obtain_url": "https://bsky.app/settings/app-passwords"
+ }
+ ],
+ "scopes": [
+ {
+ "resource": "github.repo.contents",
+ "actions": [
+ "read",
+ "write"
+ ],
+ "rationale": "Reads the existing feed file (when a feed_entry is supplied), writes the new page commit, optionally commits the updated feed, optionally appends a follow-up engagement-link commit referencing the bsky post. Lands directly on the publish branch with no PR.",
+ "provider_scope": "github-pat (coarse; full account write access)"
+ },
+ {
+ "resource": "atproto.repo",
+ "actions": [
+ "write"
+ ],
+ "rationale": "When MUNINN_BSKY_HANDLE+PASSWORD are set, creates an app.bsky.feed.post record on the user's repo announcing the published page. Detached side-effect: failure does not abort the publish.",
+ "provider_scope": "bsky-app-password (coarse; full repo write except DMs)"
+ },
+ {
+ "resource": "net.outbound",
+ "actions": [
+ "read",
+ "write"
+ ],
+ "rationale": "Talks to api.github.com for git operations, polls the eventually-served page URL on austegard.com to confirm GitHub Pages deploy, and (when bsky configured) talks to bsky.social for the announce.",
+ "provider_scope": "api.github.com, austegard.com, bsky.social"
+ }
+ ],
+ "actions": [
+ {
+ "name": "publish_and_announce",
+ "summary": "Commit a page, wait for GH Pages to serve it, optionally update the Atom feed, optionally announce on Bluesky with a follow-up engagement-link commit.",
+ "description": "Synchronous nodes: page-commit, wait-for-deploy, feed-update (when feed_entry given). Detached nodes: bsky-announce (when bsky auth supplied), engagement-link-commit (depends on bsky-announce success). The bsky_text input is validated against the 300-grapheme cap (using the bsky_limit utility) BEFORE the bsky chain fires; an over-cap text aborts only the bsky chain, not the page publish. The `deployed` boolean reflects whether the page URL was served within the deploy budget; `bsky_post` is null when the bsky chain was skipped or failed; `detached_failures` lists any background failures.",
+ "docs": {
+ "goal": "Publish an HTML page and optionally announce it on Bluesky with engagement linking.",
+ "inputs_brief": "path (req), content (req), bsky_text (req if announcing), feed_entry (optional), repo (default oaustegard/austegard.com)",
+ "outputs_brief": "{page_url, commit_sha, feed_sha, deployed, bsky_post, update_sha, detached_failures}",
+ "errors_brief": "auth_invalid, commit_failed, deploy_timeout, bsky_text_too_long, target_unreachable",
+ "example": "publish_and_announce path='blog/post.html' content='...' bsky_text='New post: ...'"
+ },
+ "invocation": {
+ "kind": "stdin-json",
+ "argv_template": [
+ "publish-and-announce"
+ ]
+ },
+ "input": {
+ "type": "object",
+ "required": [
+ "path",
+ "content"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "path": {
+ "type": "string",
+ "minLength": 1
+ },
+ "content": {
+ "type": "string",
+ "minLength": 1
+ },
+ "bsky_text": {
+ "type": "string",
+ "maxLength": 2000
+ },
+ "feed_entry": {
+ "type": "object"
+ },
+ "repo": {
+ "type": "string",
+ "pattern": "^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$",
+ "default": "oaustegard/austegard.com"
+ }
+ }
+ },
+ "output": {
+ "format": "json",
+ "schema": {
+ "type": "object",
+ "required": [
+ "page_url",
+ "commit_sha",
+ "deployed"
+ ],
+ "properties": {
+ "page_url": {
+ "type": "string",
+ "format": "uri"
+ },
+ "commit_sha": {
+ "type": "string",
+ "pattern": "^[0-9a-f]{40}$"
+ },
+ "feed_sha": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "deployed": {
+ "type": "boolean"
+ },
+ "bsky_post": {
+ "type": [
+ "object",
+ "null"
+ ],
+ "properties": {
+ "uri": {
+ "type": "string"
+ },
+ "cid": {
+ "type": "string"
+ },
+ "url": {
+ "type": "string",
+ "format": "uri"
+ },
+ "rkey": {
+ "type": "string"
+ }
+ }
+ },
+ "update_sha": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "detached_failures": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "error": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "side_effects": "destructive",
+ "idempotent": false,
+ "scopes_used": [
+ "github.repo.contents",
+ "atproto.repo",
+ "net.outbound"
+ ],
+ "error_envelope": "standard",
+ "runtime_telemetry": {}
+ }
+ ],
+ "data_boundary": {
+ "reads": [
+ {
+ "resource": "github.repo.contents",
+ "sensitivity": "low"
+ }
+ ],
+ "transmits": [
+ {
+ "to": "api.github.com",
+ "fields": [
+ "env.GH_TOKEN",
+ "input.content",
+ "input.path"
+ ],
+ "purpose": "publish page commit + feed-update commit + engagement-link commit",
+ "third_party_retention": "none-per-vendor-tos",
+ "vendor_tos_url": "https://docs.github.com/en/site-policy/privacy-policies/github-general-privacy-statement"
+ },
+ {
+ "to": "bsky.social",
+ "fields": [
+ "input.bsky_text",
+ "computed.page_url"
+ ],
+ "purpose": "announce published page; PDS federates publicly to the network firehose",
+ "third_party_retention": "persistent-indefinite"
+ },
+ {
+ "to_kind": "agent-supplied",
+ "to_constraint": "GitHub Pages domain for the input.repo (e.g. austegard.com, muninn.austegard.com). HEAD request only; no body sent. Used to confirm the page has been served before returning.",
+ "fields": [
+ "computed.page_url"
+ ],
+ "purpose": "poll page URL to confirm GH Pages deploy",
+ "third_party_retention": "unknown"
+ }
+ ],
+ "persists": []
+ },
+ "smoke": {
+ "kind": "shell",
+ "command": [
+ "python",
+ "-c",
+ "import os, json, urllib.request\ntoken = os.environ['GH_TOKEN']\nreq = urllib.request.Request('https://api.github.com/repos/oaustegard/austegard.com', headers={'Authorization': f'token {token}', 'Accept': 'application/vnd.github+json', 'User-Agent': 'blog-publish-smoke'})\nd = json.loads(urllib.request.urlopen(req).read())\nassert d['permissions']['push'], 'token lacks write access to publish-target repo'\nprint('OK: write access confirmed to', d['name'])\n"
+ ],
+ "timeout_seconds": 10,
+ "success": {
+ "exit_code": 0,
+ "stdout_regex": "OK: write access confirmed to austegard\\.com"
+ }
+ },
+ "kill_switch": {
+ "kind": "manual",
+ "instructions_url": "https://github.com/oaustegard/muninn-utilities/blob/main/manifests/blog-publish/REVOKE.md"
+ },
+ "cost": {
+ "install_fee_cents": 0,
+ "monthly_fee_cents": 0,
+ "usage_model": "external"
+ },
+ "support": {
+ "issues_url": "https://github.com/oaustegard/muninn-utilities/issues",
+ "docs_url": "https://github.com/oaustegard/muninn-utilities/blob/main/muninn_utils/blog_publish.py"
+ }
+}
diff --git a/mirrors/manifests/muninn-bsky-card.json b/mirrors/manifests/muninn-bsky-card.json
new file mode 100644
index 0000000..ff66fd4
--- /dev/null
+++ b/mirrors/manifests/muninn-bsky-card.json
@@ -0,0 +1,398 @@
+{
+ "manifest_version": "0.4",
+ "tool": {
+ "id": "muninn-bsky-card",
+ "version": "0.1.0",
+ "name": "Muninn Bluesky Card",
+ "summary": "Compose and publish Bluesky posts with rich link-card embeds (Open Graph preview). Python module; app-password auth; reads arbitrary URLs to extract OG metadata, then posts via the authenticated PDS.",
+ "description": "Library that an orchestrating agent calls to share a URL on Bluesky with a proper link card. Workflow: fetch the target URL, extract Open Graph tags, upload the card thumbnail as a blob to the user's PDS, compose UTF-8 facets for any inline links, and create the post via com.atproto.repo.createRecord. Auth is a Bluesky app password (created at bsky.app/settings/app-passwords); the password grants write access to everything except DMs and account deletion. The tool itself stores nothing; ephemeral session JWTs live only in the calling process's memory. First in the consumer-test series for install-manifest-spec v0.3 \u2014 see the muninns-inbox discussion #1 thread for the findings the writeup surfaced. Manifest moved here (from muninns-inbox/manifests/) per issue #5 \u2014 the round-1 venue mistake. Two entry shapes: the library API takes a pre-resolved `auth` dict (`handle`, `did`, `access_jwt`) from the caller, while the `python -m muninn_utils.bsky_card` CLI entrypoint resolves a session from BSKY_HANDLE + BSKY_APP_PASSWORD (see env).",
+ "homepage": "https://github.com/oaustegard/muninn-utilities/blob/main/muninn_utils/bsky_card.py",
+ "author": {
+ "name": "Muninn (raven of memory; agent operating on behalf of Oskar Austegard)",
+ "url": "https://muninn.austegard.com"
+ },
+ "license": "MIT",
+ "tags": [
+ "bluesky",
+ "atproto",
+ "social",
+ "posting",
+ "link-card"
+ ]
+ },
+ "runtime": {
+ "kind": "python-module",
+ "install": {
+ "method": "preinstalled",
+ "locator": {
+ "kind": "python-module",
+ "module": "muninn_utils.bsky_card"
+ }
+ },
+ "entrypoint": {
+ "command": [
+ "python",
+ "-m",
+ "muninn_utils.bsky_card"
+ ]
+ }
+ },
+ "env": [
+ {
+ "name": "BSKY_HANDLE",
+ "prompt": "The Bluesky handle the CLI authenticates as, e.g. 'austegard.com'. Required by the `python -m muninn_utils.bsky_card` entrypoint (whoami / post-link / delete-post). The library API takes a pre-resolved `auth` dict instead and ignores this var.",
+ "secret": false,
+ "required": true,
+ "validation_regex": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$",
+ "obtain_url": "https://bsky.app/settings/account"
+ },
+ {
+ "name": "BSKY_APP_PASSWORD",
+ "prompt": "App password for BSKY_HANDLE. Format is four hyphen-separated four-character groups. Treat as a secret. Required by the CLI entrypoint; bsky app passwords cannot be programmatically revoked (the kill switch is the app-passwords settings page).",
+ "secret": true,
+ "required": true,
+ "validation_regex": "^[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}$",
+ "obtain_url": "https://bsky.app/settings/app-passwords"
+ },
+ {
+ "name": "BSKY_PDS",
+ "prompt": "Optional PDS base URL override. Defaults to https://bsky.social. Set only when authenticating against a self-hosted or non-default PDS.",
+ "secret": false,
+ "required": false,
+ "validation_regex": "^https?://.+",
+ "obtain_url": "https://atproto.com/guides/self-hosting"
+ }
+ ],
+ "scopes": [
+ {
+ "resource": "atproto.repo",
+ "actions": [
+ "read",
+ "write"
+ ],
+ "rationale": "Creates and deletes records (app.bsky.feed.post) in the user's repo, and uploads blob attachments for card thumbnails.",
+ "provider_scope": "app-password (coarse)"
+ },
+ {
+ "resource": "atproto.identity",
+ "actions": [
+ "read"
+ ],
+ "rationale": "Resolves the user's handle to a DID at startup so the smoke test can confirm auth resolves to the expected account.",
+ "provider_scope": "app-password (coarse)"
+ },
+ {
+ "resource": "net.outbound",
+ "actions": [
+ "read"
+ ],
+ "rationale": "Two outbound destinations: (1) the configured PDS (default bsky.social) for atproto operations, and (2) ARBITRARY user-supplied URLs fetched server-side to extract Open Graph metadata. The second is the wider blast radius \u2014 any URL the agent shares is fetched by this tool.",
+ "provider_scope": "*"
+ }
+ ],
+ "actions": [
+ {
+ "name": "whoami",
+ "summary": "Authenticate, resolve handle to DID, and return both. Read-only.",
+ "description": "Calls com.atproto.server.createSession with the configured handle + app password, then com.atproto.identity.resolveHandle to confirm the handle resolves to the same DID the session returned. Used as the smoke test.",
+ "docs": {
+ "goal": "Verify auth and confirm the configured handle resolves to a real DID.",
+ "inputs_brief": "(none)",
+ "outputs_brief": "{handle, did, pds}",
+ "errors_brief": "auth_invalid, handle_not_found, network_unreachable",
+ "example": "whoami"
+ },
+ "invocation": {
+ "kind": "subcommand",
+ "argv_template": [
+ "whoami"
+ ]
+ },
+ "output": {
+ "format": "json",
+ "schema": {
+ "type": "object",
+ "required": [
+ "handle",
+ "did"
+ ],
+ "properties": {
+ "handle": {
+ "type": "string"
+ },
+ "did": {
+ "type": "string",
+ "pattern": "^did:"
+ },
+ "pds": {
+ "type": "string",
+ "format": "uri"
+ }
+ }
+ }
+ },
+ "side_effects": "read",
+ "idempotent": true,
+ "scopes_used": [
+ "atproto.identity"
+ ],
+ "error_envelope": "standard",
+ "examples": [
+ {
+ "description": "Healthy auth.",
+ "input": {},
+ "output": {
+ "handle": "austegard.com",
+ "did": "did:plc:abc123...",
+ "pds": "https://bsky.social"
+ }
+ }
+ ],
+ "runtime_telemetry": {}
+ },
+ {
+ "name": "post_link",
+ "summary": "Post a URL to Bluesky with an automatically-generated link card.",
+ "description": "Fetches the URL, extracts Open Graph (og:title / og:description / og:image), uploads the image as a blob to the user's PDS, computes UTF-8 facets for any inline links in the post text, and creates an app.bsky.feed.post record with an external embed. Destructive (a posted message is publicly visible immediately and federates to the wider AppView) but reversible via delete_post on the returned URI.",
+ "docs": {
+ "goal": "Share a URL on Bluesky with a proper card preview.",
+ "inputs_brief": "text (\u2264300 graphemes), url, og_overrides? (manual title/description/image), languages? (BCP-47 array)",
+ "outputs_brief": "{uri, cid, url}",
+ "errors_brief": "text_too_long, url_unreachable, blob_upload_failed, auth_invalid, rate_limited",
+ "example": "post_link text='New post on the manifest spec' url='https://muninn.austegard.com/perch/...'"
+ },
+ "invocation": {
+ "kind": "stdin-json",
+ "argv_template": [
+ "post-link"
+ ]
+ },
+ "input": {
+ "type": "object",
+ "required": [
+ "text",
+ "url"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "text": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Post text. Library enforces 300-grapheme cap (NOT len(); emoji and combining marks count differently)."
+ },
+ "url": {
+ "type": "string",
+ "format": "uri"
+ },
+ "og_overrides": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "title": {
+ "type": "string",
+ "maxLength": 300
+ },
+ "description": {
+ "type": "string",
+ "maxLength": 1000
+ },
+ "image": {
+ "type": "string",
+ "format": "uri"
+ }
+ }
+ },
+ "languages": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "pattern": "^[a-z]{2,3}(-[A-Z]{2})?$"
+ },
+ "maxItems": 4,
+ "default": [
+ "en"
+ ]
+ }
+ }
+ },
+ "output": {
+ "format": "json",
+ "schema": {
+ "type": "object",
+ "required": [
+ "uri",
+ "cid"
+ ],
+ "properties": {
+ "uri": {
+ "type": "string",
+ "pattern": "^at://"
+ },
+ "cid": {
+ "type": "string"
+ },
+ "url": {
+ "type": "string",
+ "format": "uri",
+ "description": "Convenience: https://bsky.app/profile/{handle}/post/{rkey}"
+ }
+ }
+ }
+ },
+ "side_effects": "destructive",
+ "idempotent": false,
+ "scopes_used": [
+ "atproto.repo",
+ "net.outbound"
+ ],
+ "error_envelope": "standard",
+ "examples": [
+ {
+ "description": "Share a blog post.",
+ "input": {
+ "text": "I wrote a manifest for one of my own tools. Findings in the muninns-inbox thread.",
+ "url": "https://github.com/oaustegard/muninns-inbox/discussions/1"
+ },
+ "output": {
+ "uri": "at://did:plc:abc.../app.bsky.feed.post/3l5...",
+ "cid": "bafy...",
+ "url": "https://bsky.app/profile/austegard.com/post/3l5..."
+ }
+ }
+ ],
+ "runtime_telemetry": {}
+ },
+ {
+ "name": "delete_post",
+ "summary": "Delete a previously-created post by AT-URI.",
+ "description": "Calls com.atproto.repo.deleteRecord on a post URI returned by post_link. Idempotent (deleting a non-existent record is a no-op at the AppView level). Network-propagation lag means the post may remain visible briefly after delete returns.",
+ "docs": {
+ "goal": "Retract a Bluesky post by AT-URI.",
+ "inputs_brief": "uri (at:// URI from post_link)",
+ "outputs_brief": "{uri, deleted: true}",
+ "errors_brief": "uri_invalid, not_owned, auth_invalid",
+ "example": "delete-post uri='at://did:plc:abc.../app.bsky.feed.post/3l5...'"
+ },
+ "invocation": {
+ "kind": "stdin-json",
+ "argv_template": [
+ "delete-post"
+ ]
+ },
+ "input": {
+ "type": "object",
+ "required": [
+ "uri"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "uri": {
+ "type": "string",
+ "pattern": "^at://"
+ }
+ }
+ },
+ "output": {
+ "format": "json",
+ "schema": {
+ "type": "object",
+ "required": [
+ "uri",
+ "deleted"
+ ],
+ "properties": {
+ "uri": {
+ "type": "string"
+ },
+ "deleted": {
+ "type": "boolean"
+ }
+ }
+ }
+ },
+ "side_effects": "destructive",
+ "idempotent": true,
+ "scopes_used": [
+ "atproto.repo"
+ ],
+ "error_envelope": "standard",
+ "runtime_telemetry": {}
+ }
+ ],
+ "verify": {
+ "sla": {
+ "p50_latency_ms": 1200,
+ "p95_latency_ms": 4000,
+ "error_rate_max": 0.05
+ },
+ "schedule": {
+ "cadence": "weekly",
+ "on_install": false
+ }
+ },
+ "data_boundary": {
+ "reads": [
+ {
+ "resource": "atproto.repo.posts",
+ "sensitivity": "low"
+ },
+ {
+ "resource": "external.url_content",
+ "sensitivity": "low"
+ }
+ ],
+ "transmits": [
+ {
+ "to": "bsky.social",
+ "fields": [
+ "atproto.repo.posts/text",
+ "atproto.repo.posts/embed.external.uri",
+ "atproto.repo.posts/embed.external.title",
+ "atproto.repo.posts/embed.external.description",
+ "atproto.repo.posts/embed.external.thumb"
+ ],
+ "purpose": "publish post + OG-card thumbnail blob to user's PDS; PDS federates publicly to the network firehose. Posts are public by design.",
+ "third_party_retention": "persistent-indefinite"
+ },
+ {
+ "to_kind": "agent-supplied",
+ "to_constraint": "URLs the caller asks to share (post_link.url) and the og:image URL extracted from that page. Both fetched server-side by this tool to extract Open Graph metadata and thumbnail bytes.",
+ "fields": [
+ "input.url",
+ "fetched.og_image_url"
+ ],
+ "purpose": "fetch OG metadata and thumbnail image from the caller-supplied URL",
+ "third_party_retention": "unknown"
+ }
+ ],
+ "persists": [],
+ "retention": {
+ "tool_local_days": 0
+ }
+ },
+ "smoke": {
+ "kind": "action-call",
+ "action": "whoami",
+ "arguments": {},
+ "timeout_seconds": 10,
+ "success": {
+ "no_error_field": true,
+ "json_pointer_equals": {
+ "/handle": "${BSKY_HANDLE}"
+ }
+ }
+ },
+ "kill_switch": {
+ "kind": "url",
+ "url": "https://bsky.app/settings/app-passwords"
+ },
+ "cost": {
+ "install_fee_cents": 0,
+ "monthly_fee_cents": 0,
+ "usage_model": "none"
+ },
+ "support": {
+ "issues_url": "https://github.com/oaustegard/muninn-utilities/issues",
+ "docs_url": "https://github.com/oaustegard/muninn-utilities/blob/main/muninn_utils/bsky_card.py"
+ }
+}
diff --git a/mirrors/manifests/muninn-bsky-limit.json b/mirrors/manifests/muninn-bsky-limit.json
new file mode 100644
index 0000000..ca23ca7
--- /dev/null
+++ b/mirrors/manifests/muninn-bsky-limit.json
@@ -0,0 +1,231 @@
+{
+ "manifest_version": "0.4",
+ "tool": {
+ "id": "muninn-bsky-limit",
+ "version": "0.1.0",
+ "name": "Muninn bsky_limit",
+ "summary": "Bluesky 300-grapheme length checker and truncator. len() lies on emoji and ZWJ sequences; this counts graphemes correctly and truncates at the last whitespace boundary.",
+ "description": "Two-function helper used by anything that posts to Bluesky. fits(text, limit=300) returns whether the text fits the AppView's grapheme cap; truncate(text, limit=300, suffix='\u2026') walks back to the last whitespace under the cap and appends the suffix. Pure compute: no I/O, no environment, no state. Depends on the `grapheme` PyPI package, which the module pip-installs at import time if missing (the only side-effect surface). Manifested here as the shared-credential / shared-dependency reuse case for Bluesky utilities (bsky_card, blog_publish, whtwnd) and as the v0.3-spec stress-test of 'a tool whose only declarable scope is compute.local'.",
+ "homepage": "https://github.com/oaustegard/muninn-utilities/blob/main/muninn_utils/bsky_limit.py",
+ "author": {
+ "name": "Muninn (raven of memory; agent operating on behalf of Oskar Austegard)",
+ "url": "https://muninn.austegard.com"
+ },
+ "license": "MIT",
+ "tags": [
+ "bluesky",
+ "atproto",
+ "grapheme",
+ "text-utility",
+ "length-check"
+ ]
+ },
+ "runtime": {
+ "kind": "python-module",
+ "install": {
+ "method": "preinstalled",
+ "locator": {
+ "kind": "python-module",
+ "module": "muninn_utils.bsky_limit"
+ }
+ },
+ "entrypoint": {
+ "command": [
+ "python",
+ "-m",
+ "muninn_utils.bsky_limit"
+ ]
+ }
+ },
+ "scopes": [
+ {
+ "resource": "compute.local",
+ "actions": [
+ "read"
+ ],
+ "rationale": "Pure string computation. The only non-pure surface is a one-time pip install of `grapheme` if absent at import time."
+ }
+ ],
+ "actions": [
+ {
+ "name": "fits",
+ "summary": "Return whether the given text fits the Bluesky 300-grapheme post limit.",
+ "description": "Counts graphemes (not codepoints, not bytes) using the `grapheme` package. The default limit is 300 to match Bluesky's AppView; callers may override for other surfaces.",
+ "docs": {
+ "goal": "Check whether text fits Bluesky's 300-grapheme cap.",
+ "inputs_brief": "text (req), limit (int, default 300)",
+ "outputs_brief": "{fits: bool, length: int}",
+ "errors_brief": "(none \u2014 pure compute)",
+ "example": "fits text='Hello \ud83d\udc4b' limit=300"
+ },
+ "invocation": {
+ "kind": "stdin-json",
+ "argv_template": [
+ "fits"
+ ]
+ },
+ "input": {
+ "type": "object",
+ "required": [
+ "text"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "text": {
+ "type": "string"
+ },
+ "limit": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 10000,
+ "default": 300
+ }
+ }
+ },
+ "output": {
+ "format": "json",
+ "schema": {
+ "type": "object",
+ "required": [
+ "fits",
+ "length"
+ ],
+ "properties": {
+ "fits": {
+ "type": "boolean"
+ },
+ "length": {
+ "type": "integer",
+ "minimum": 0
+ }
+ }
+ }
+ },
+ "side_effects": "none",
+ "idempotent": true,
+ "scopes_used": [
+ "compute.local"
+ ],
+ "error_envelope": "standard",
+ "examples": [
+ {
+ "description": "Plain ASCII fits.",
+ "input": {
+ "text": "Hello world"
+ },
+ "output": {
+ "fits": true,
+ "length": 11
+ }
+ },
+ {
+ "description": "Emoji counts as one grapheme, not four bytes.",
+ "input": {
+ "text": "\ud83d\udc4b"
+ },
+ "output": {
+ "fits": true,
+ "length": 1
+ }
+ }
+ ],
+ "runtime_telemetry": {}
+ },
+ {
+ "name": "truncate",
+ "summary": "Truncate text to fit the grapheme limit, walking back to the last whitespace boundary if possible.",
+ "description": "If the text already fits, return it unchanged. Otherwise slice to (limit - len(suffix)) graphemes, walk back to the last space/newline/tab, and append the suffix. Returns at most `limit` graphemes total.",
+ "docs": {
+ "goal": "Trim text to fit Bluesky's grapheme cap without breaking words.",
+ "inputs_brief": "text (req), limit (int, default 300), suffix (string, default '\u2026')",
+ "outputs_brief": "{text: string, length: int, truncated: bool}",
+ "errors_brief": "(none \u2014 pure compute)",
+ "example": "truncate text='very long...' limit=50"
+ },
+ "invocation": {
+ "kind": "stdin-json",
+ "argv_template": [
+ "truncate"
+ ]
+ },
+ "input": {
+ "type": "object",
+ "required": [
+ "text"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "text": {
+ "type": "string"
+ },
+ "limit": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 10000,
+ "default": 300
+ },
+ "suffix": {
+ "type": "string",
+ "default": "\u2026"
+ }
+ }
+ },
+ "output": {
+ "format": "json",
+ "schema": {
+ "type": "object",
+ "required": [
+ "text",
+ "length",
+ "truncated"
+ ],
+ "properties": {
+ "text": {
+ "type": "string"
+ },
+ "length": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "truncated": {
+ "type": "boolean"
+ }
+ }
+ }
+ },
+ "side_effects": "none",
+ "idempotent": true,
+ "scopes_used": [
+ "compute.local"
+ ],
+ "error_envelope": "standard",
+ "runtime_telemetry": {}
+ }
+ ],
+ "smoke": {
+ "kind": "shell",
+ "command": [
+ "python",
+ "-c",
+ "from muninn_utils.bsky_limit import fits, truncate\nassert fits('hello') is True\nassert fits('x' * 301) is False\nassert truncate('x' * 400).endswith('\u2026')\nprint('OK')\n"
+ ],
+ "timeout_seconds": 10,
+ "success": {
+ "exit_code": 0,
+ "stdout_regex": "^OK$"
+ }
+ },
+ "kill_switch": {
+ "kind": "manual",
+ "instructions_url": "https://github.com/oaustegard/muninn-utilities/blob/main/manifests/bsky-limit/REVOKE.md"
+ },
+ "cost": {
+ "install_fee_cents": 0,
+ "monthly_fee_cents": 0,
+ "usage_model": "none"
+ },
+ "support": {
+ "issues_url": "https://github.com/oaustegard/muninn-utilities/issues",
+ "docs_url": "https://github.com/oaustegard/muninn-utilities/blob/main/muninn_utils/bsky_limit.py"
+ }
+}
diff --git a/mirrors/manifests/muninn-issue-close.json b/mirrors/manifests/muninn-issue-close.json
new file mode 100644
index 0000000..d6113c7
--- /dev/null
+++ b/mirrors/manifests/muninn-issue-close.json
@@ -0,0 +1,287 @@
+{
+ "manifest_version": "0.4",
+ "tool": {
+ "id": "muninn-issue-close",
+ "version": "0.1.0",
+ "name": "Muninn issue_close",
+ "summary": "Close a GitHub issue with a learning synthesis. Posts the synthesis as a closing comment, then writes it as a `decision` memory tagged with the issue number \u2014 encoded as a flowing DAG so the close ack returns the moment GitHub returns 2xx, while the memory write happens detached.",
+ "description": "Two-artifact close: GitHub issue gets the implementation log (the closing comment), Muninn's memory store gets the behavioral learning (the decision memory). Wraps a `flowing` graph so synthesis-text validation runs BEFORE any GitHub call fires, the close-issue ack is the terminal node (returns immediately on 2xx), and the memory-store + optional pending-test-verification run as detached side-effects whose failure populates `flow.detached_failures` rather than bubbling up. Same shape as `perch_publish` (single primary credential, structured comment) but writes to a different surface (issues, not pages) and with a paired Turso write per close. Issue #5 calls this out as the credential-reuse-with-perch_publish test.",
+ "homepage": "https://github.com/oaustegard/muninn-utilities/blob/main/muninn_utils/issue_close.py",
+ "author": {
+ "name": "Muninn (raven of memory; agent operating on behalf of Oskar Austegard)",
+ "url": "https://muninn.austegard.com"
+ },
+ "license": "MIT",
+ "tags": [
+ "github",
+ "issues",
+ "decision-memory",
+ "learning-synthesis",
+ "flowing"
+ ]
+ },
+ "runtime": {
+ "kind": "python-module",
+ "install": {
+ "method": "preinstalled",
+ "locator": {
+ "kind": "python-module",
+ "module": "muninn_utils.issue_close"
+ }
+ },
+ "entrypoint": {
+ "command": [
+ "python",
+ "-m",
+ "muninn_utils.issue_close"
+ ]
+ }
+ },
+ "env": [
+ {
+ "name": "GH_TOKEN",
+ "prompt": "GitHub personal access token. Needs write access (issues scope) to the target repo. Classic PAT with repo scope works; fine-grained PAT with explicit per-repo Issues:write is preferred. The same token is used by perch_publish, blog_publish, perch_triage, and verify_patch \u2014 share-by-default is intentional.",
+ "secret": true,
+ "required": true,
+ "validation_regex": "^(ghp_[A-Za-z0-9]{36}|github_pat_[A-Za-z0-9_]+)$",
+ "obtain_url": "https://github.com/settings/personal-access-tokens"
+ },
+ {
+ "name": "GITHUB_TOKEN",
+ "prompt": "Optional fallback for GH_TOKEN. The source reads `os.environ.get('GH_TOKEN') or os.environ.get('GITHUB_TOKEN')`.",
+ "secret": true,
+ "required": false,
+ "validation_regex": "^(ghp_[A-Za-z0-9]{36}|github_pat_[A-Za-z0-9_]+)$",
+ "obtain_url": "https://github.com/settings/personal-access-tokens"
+ },
+ {
+ "name": "TURSO_TOKEN",
+ "prompt": "Turso libSQL auth token for the Muninn memory database. The decision memory is written here. Required.",
+ "secret": true,
+ "required": true,
+ "obtain_url": "https://app.turso.tech/"
+ },
+ {
+ "name": "TURSO_URL",
+ "prompt": "Hostname of the Muninn memory libSQL database, e.g. 'mydb-username.turso.io'.",
+ "secret": false,
+ "required": true,
+ "validation_regex": "^[a-z0-9-]+\\.[a-z0-9-]+\\.turso\\.io$"
+ }
+ ],
+ "scopes": [
+ {
+ "resource": "github.issues",
+ "actions": [
+ "read",
+ "write"
+ ],
+ "rationale": "Posts a closing comment and toggles state to closed on the target issue. Reads the current issue state to decide whether the close is a no-op.",
+ "provider_scope": "github-pat (coarse; full account write access)"
+ },
+ {
+ "resource": "memory.tracking",
+ "actions": [
+ "write"
+ ],
+ "rationale": "Stores the learning synthesis as a `decision`-typed memory tagged `issue-N` and the short-form repo identifier so that future recall can find it from either side of the issue/memory pair.",
+ "provider_scope": "turso-libsql-token (coarse; full DB access)"
+ },
+ {
+ "resource": "net.outbound",
+ "actions": [
+ "read",
+ "write"
+ ],
+ "rationale": "Talks to api.github.com (REST) for issue read/comment/close and to the configured Turso libSQL host for the memory write. No other outbound destinations.",
+ "provider_scope": "api.github.com, *.turso.io"
+ }
+ ],
+ "actions": [
+ {
+ "name": "close",
+ "summary": "Validate synthesis, close the GitHub issue with it as the closing comment, and store the decision memory in the background.",
+ "description": "Synchronously: validates the synthesis is non-empty, posts it as a comment on the issue, closes the issue. The close-ack (issue_url + comment_url) is returned immediately. Detached: writes a `decision`-type memory with the synthesis body and tags `issue-N`, ``, plus any extra_tags. If pending_test=true, an additional detached node verifies the pending-test contract on the freshly-stored memory. Detached failures populate the returned `detached_failures` array; they do NOT raise. Idempotent at the GitHub layer (closing an already-closed issue returns the same shape) but NOT at the memory layer (each close stores a new memory).",
+ "docs": {
+ "goal": "Close a GitHub issue with a learning synthesis and persist the synthesis as a decision memory.",
+ "inputs_brief": "number (req), synthesis (req), repo (default oaustegard/claude-skills), pending_test (bool), extra_tags (string[])",
+ "outputs_brief": "{issue_url, comment_url, memory_id, pending_test_applied, detached_failures}",
+ "errors_brief": "synthesis_empty, issue_not_found, auth_invalid, close_failed",
+ "example": "close number=619 synthesis='Pattern X works because Y' repo=oaustegard/claude-skills"
+ },
+ "invocation": {
+ "kind": "stdin-json",
+ "argv_template": [
+ "close"
+ ]
+ },
+ "input": {
+ "type": "object",
+ "required": [
+ "number",
+ "synthesis"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "number": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "synthesis": {
+ "type": "string",
+ "minLength": 1
+ },
+ "repo": {
+ "type": "string",
+ "pattern": "^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$",
+ "default": "oaustegard/claude-skills"
+ },
+ "pending_test": {
+ "type": "boolean",
+ "default": false
+ },
+ "extra_tags": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "minLength": 1
+ },
+ "maxItems": 16,
+ "default": []
+ }
+ }
+ },
+ "output": {
+ "format": "json",
+ "schema": {
+ "type": "object",
+ "required": [
+ "issue_url",
+ "comment_url",
+ "memory_id"
+ ],
+ "properties": {
+ "issue_url": {
+ "type": "string",
+ "format": "uri"
+ },
+ "comment_url": {
+ "type": "string",
+ "format": "uri"
+ },
+ "memory_id": {
+ "type": "string"
+ },
+ "pending_test_applied": {
+ "type": "boolean"
+ },
+ "detached_failures": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "error": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "side_effects": "destructive",
+ "idempotent": false,
+ "scopes_used": [
+ "github.issues",
+ "memory.tracking",
+ "net.outbound"
+ ],
+ "error_envelope": "standard",
+ "examples": [
+ {
+ "description": "Close issue #619 with a synthesis.",
+ "input": {
+ "number": 619,
+ "synthesis": "Refactor pattern X works because of Y. Constraint: Z.",
+ "repo": "oaustegard/claude-skills",
+ "extra_tags": [
+ "flowing",
+ "refactor"
+ ]
+ },
+ "output": {
+ "issue_url": "https://github.com/oaustegard/claude-skills/issues/619",
+ "comment_url": "https://github.com/oaustegard/claude-skills/issues/619#issuecomment-12345",
+ "memory_id": "abc12345",
+ "pending_test_applied": false,
+ "detached_failures": []
+ }
+ }
+ ],
+ "runtime_telemetry": {}
+ }
+ ],
+ "data_boundary": {
+ "reads": [
+ {
+ "resource": "github.issues",
+ "sensitivity": "low"
+ }
+ ],
+ "transmits": [
+ {
+ "to": "api.github.com",
+ "fields": [
+ "env.GH_TOKEN",
+ "input.synthesis"
+ ],
+ "purpose": "post closing comment + toggle issue state",
+ "third_party_retention": "none-per-vendor-tos",
+ "vendor_tos_url": "https://docs.github.com/en/site-policy/privacy-policies/github-general-privacy-statement"
+ }
+ ],
+ "persists": [
+ {
+ "where": "tool_local",
+ "fields": [
+ "synthesis",
+ "issue_number",
+ "repo",
+ "extra_tags"
+ ]
+ }
+ ],
+ "retention": {
+ "tool_local_days": 365
+ }
+ },
+ "smoke": {
+ "kind": "shell",
+ "command": [
+ "python",
+ "-c",
+ "import os, json, urllib.request\ntoken = os.environ['GH_TOKEN']\nreq = urllib.request.Request('https://api.github.com/repos/oaustegard/claude-skills', headers={'Authorization': f'token {token}', 'Accept': 'application/vnd.github+json', 'User-Agent': 'issue-close-smoke'})\nd = json.loads(urllib.request.urlopen(req).read())\nassert d['permissions']['push'], 'token lacks write access (needed for issue close)'\nprint('OK: write access confirmed to', d['name'])\n"
+ ],
+ "timeout_seconds": 10,
+ "success": {
+ "exit_code": 0,
+ "stdout_regex": "OK: write access confirmed to claude-skills"
+ }
+ },
+ "kill_switch": {
+ "kind": "manual",
+ "instructions_url": "https://github.com/oaustegard/muninn-utilities/blob/main/manifests/issue-close/REVOKE.md"
+ },
+ "cost": {
+ "install_fee_cents": 0,
+ "monthly_fee_cents": 0,
+ "usage_model": "external"
+ },
+ "support": {
+ "issues_url": "https://github.com/oaustegard/muninn-utilities/issues",
+ "docs_url": "https://github.com/oaustegard/muninn-utilities/blob/main/muninn_utils/issue_close.py"
+ }
+}
diff --git a/mirrors/manifests/muninn-memory-tfidf.json b/mirrors/manifests/muninn-memory-tfidf.json
new file mode 100644
index 0000000..ace9694
--- /dev/null
+++ b/mirrors/manifests/muninn-memory-tfidf.json
@@ -0,0 +1,189 @@
+{
+ "manifest_version": "0.4",
+ "tool": {
+ "id": "muninn-memory-tfidf",
+ "version": "0.1.0",
+ "name": "Muninn memory_tfidf",
+ "summary": "TF-IDF index over Muninn's memory summaries. Read-only similarity search, near-duplicate detection, clustering, and outlier identification across the memory store.",
+ "description": "Loads all memory summaries from Turso once, builds a sklearn TfidfVectorizer + cosine-similarity matrix, and exposes four read-only queries: duplicates() (pairs above a similarity threshold), similar(id) (top-N for a given memory), clusters() (connected components above threshold), outliers() (memories with low max-similarity to anything else). Build is in-memory and ephemeral; nothing persists between calls in the tool's own storage. Authored as the deliberate **minimum-honest-manifest** consumer test for install-manifest-spec v0.3 \u2014 every required field is present, nothing optional is added past what the tool actually does.",
+ "homepage": "https://github.com/oaustegard/muninn-utilities/blob/main/muninn_utils/memory_tfidf.py",
+ "author": {
+ "name": "Muninn (raven of memory; agent operating on behalf of Oskar Austegard)",
+ "url": "https://muninn.austegard.com"
+ },
+ "license": "MIT",
+ "tags": [
+ "tf-idf",
+ "similarity",
+ "clustering",
+ "memory",
+ "read-only"
+ ]
+ },
+ "runtime": {
+ "kind": "python-module",
+ "install": {
+ "method": "preinstalled",
+ "locator": {
+ "kind": "python-module",
+ "module": "muninn_utils.memory_tfidf"
+ }
+ },
+ "entrypoint": {
+ "command": [
+ "python",
+ "-m",
+ "muninn_utils.memory_tfidf"
+ ]
+ }
+ },
+ "env": [
+ {
+ "name": "TURSO_TOKEN",
+ "prompt": "Turso libSQL auth token for the memory database. Read-only access is sufficient \u2014 this tool never writes. The token is a coarse credential; treat as a secret.",
+ "secret": true,
+ "required": true,
+ "obtain_url": "https://app.turso.tech/"
+ },
+ {
+ "name": "TURSO_URL",
+ "prompt": "Hostname of the Turso libSQL database, e.g. 'mydb-username.turso.io'. The tool reads memories via the libSQL HTTP pipeline endpoint.",
+ "secret": false,
+ "required": true,
+ "validation_regex": "^[a-z0-9-]+\\.[a-z0-9-]+\\.turso\\.io$"
+ }
+ ],
+ "scopes": [
+ {
+ "resource": "memory.tracking",
+ "actions": [
+ "read"
+ ],
+ "rationale": "Reads all memory summaries (id, summary, tags, type) from the Turso DB to build the index. Never writes.",
+ "provider_scope": "turso-libsql-token (coarse; full DB access)"
+ },
+ {
+ "resource": "net.outbound",
+ "actions": [
+ "read"
+ ],
+ "rationale": "Talks to the configured Turso libSQL host for the initial fetch. No other outbound destinations.",
+ "provider_scope": "*.turso.io"
+ }
+ ],
+ "actions": [
+ {
+ "name": "build_and_query",
+ "summary": "Fetch all memory summaries, build the TF-IDF index, and return a query result (duplicates / similar / clusters / outliers).",
+ "description": "Single combined action because the build is the expensive step (one SELECT over the full memory table plus the TfidfVectorizer fit). All four query modes share the same matrix; the action takes a `mode` discriminant and the parameters relevant to that mode. The matrix is not cached between invocations \u2014 each call rebuilds. Read-only at every layer.",
+ "docs": {
+ "goal": "Run a TF-IDF similarity query over the memory store.",
+ "inputs_brief": "mode (duplicates|similar|clusters|outliers), threshold (float), id (for similar), n (for similar/outliers)",
+ "outputs_brief": "{mode: string, results: array, build_time_ms: number, total_memories: int}",
+ "errors_brief": "tracking_unconfigured (TURSO_* not set), tracking_unreachable, mode_unknown, id_not_found",
+ "example": "build_and_query mode=duplicates threshold=0.8"
+ },
+ "invocation": {
+ "kind": "stdin-json",
+ "argv_template": [
+ "build-and-query"
+ ]
+ },
+ "input": {
+ "type": "object",
+ "required": [
+ "mode"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "mode": {
+ "type": "string",
+ "enum": [
+ "duplicates",
+ "similar",
+ "clusters",
+ "outliers"
+ ]
+ },
+ "threshold": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 1,
+ "default": 0.8
+ },
+ "id": {
+ "type": "string",
+ "description": "Memory id; required when mode=similar."
+ },
+ "n": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 200,
+ "default": 5
+ }
+ }
+ },
+ "output": {
+ "format": "json",
+ "schema": {
+ "type": "object",
+ "required": [
+ "mode",
+ "results",
+ "build_time_ms",
+ "total_memories"
+ ],
+ "properties": {
+ "mode": {
+ "type": "string"
+ },
+ "results": {
+ "type": "array"
+ },
+ "build_time_ms": {
+ "type": "number"
+ },
+ "total_memories": {
+ "type": "integer",
+ "minimum": 0
+ }
+ }
+ }
+ },
+ "side_effects": "read",
+ "idempotent": true,
+ "scopes_used": [
+ "memory.tracking",
+ "net.outbound"
+ ],
+ "error_envelope": "standard",
+ "runtime_telemetry": {}
+ }
+ ],
+ "smoke": {
+ "kind": "shell",
+ "command": [
+ "python",
+ "-c",
+ "from muninn_utils.memory_tfidf import MemoryIndex\nidx = MemoryIndex()\nidx.build(memories=[{'id': 'a', 'summary': 'apple banana', 'tags': []}, {'id': 'b', 'summary': 'banana cherry', 'tags': []}])\nassert len(idx.ids) == 2\nprint('OK')\n"
+ ],
+ "timeout_seconds": 10,
+ "success": {
+ "exit_code": 0,
+ "stdout_regex": "^OK$"
+ }
+ },
+ "kill_switch": {
+ "kind": "manual",
+ "instructions_url": "https://github.com/oaustegard/muninn-utilities/blob/main/manifests/memory-tfidf/REVOKE.md"
+ },
+ "cost": {
+ "install_fee_cents": 0,
+ "monthly_fee_cents": 0,
+ "usage_model": "none"
+ },
+ "support": {
+ "issues_url": "https://github.com/oaustegard/muninn-utilities/issues",
+ "docs_url": "https://github.com/oaustegard/muninn-utilities/blob/main/muninn_utils/memory_tfidf.py"
+ }
+}
diff --git a/mirrors/manifests/muninn-news-watch.json b/mirrors/manifests/muninn-news-watch.json
new file mode 100644
index 0000000..ab9de7a
--- /dev/null
+++ b/mirrors/manifests/muninn-news-watch.json
@@ -0,0 +1,415 @@
+{
+ "manifest_version": "0.4",
+ "tool": {
+ "id": "muninn-news-watch",
+ "version": "0.1.0",
+ "name": "Muninn news_watch",
+ "summary": "Watch claude.com/blog for new posts during Daily Perch. Pure parsing + watermark state; HTTP fetching is delegated to the caller's web_fetch tool (claude.com WAFs raw container egress).",
+ "description": "Five functions: parse_claude_blog(content) extracts post links, dates and categories from rendered blog-index content; filter_new(posts, last_seen) returns posts strictly newer than the watermark; get_last_seen/set_last_seen read and write a single ISO date in Turso config (key 'claude-blog-last-seen-iso', category 'ops'); format_for_report(new_posts) renders HTML rows for the perch report. First run (last_seen is None) returns no new posts so the seed run doesn't alert on already-historical content; the watermark advances even when no new posts are found to avoid re-scanning the same back-window. The tool itself does NOT make outbound HTTP fetches against claude.com — that's the caller's web_fetch (claude.com 403s raw HTTP from container egress); the tool only parses content the caller already fetched and reads/writes the watermark in Turso.",
+ "homepage": "https://github.com/oaustegard/muninn-utilities/blob/main/muninn_utils/news_watch.py",
+ "author": {
+ "name": "Muninn (raven of memory; agent operating on behalf of Oskar Austegard)",
+ "url": "https://muninn.austegard.com"
+ },
+ "license": "MIT",
+ "tags": [
+ "news",
+ "watermark",
+ "claude-blog",
+ "parsing",
+ "perch"
+ ]
+ },
+ "runtime": {
+ "kind": "python-module",
+ "install": {
+ "method": "preinstalled",
+ "locator": {
+ "kind": "python-module",
+ "module": "muninn_utils.news_watch"
+ }
+ },
+ "entrypoint": {
+ "command": [
+ "python",
+ "-m",
+ "muninn_utils.news_watch"
+ ]
+ }
+ },
+ "env": [
+ {
+ "name": "TURSO_TOKEN",
+ "prompt": "Turso libSQL auth token for the Muninn memory database. Required for the watermark read/write (get_last_seen / set_last_seen route through `scripts.config_get`/`config_set`, which uses these credentials). Treat as a secret.",
+ "secret": true,
+ "required": true,
+ "obtain_url": "https://app.turso.tech/"
+ },
+ {
+ "name": "TURSO_URL",
+ "prompt": "Hostname of the Muninn memory libSQL database, e.g. 'mydb-username.turso.io'.",
+ "secret": false,
+ "required": true,
+ "validation_regex": "^[a-z0-9-]+\\.[a-z0-9-]+\\.turso\\.io$"
+ }
+ ],
+ "scopes": [
+ {
+ "resource": "memory.tracking",
+ "actions": [
+ "read",
+ "write"
+ ],
+ "rationale": "Reads and writes a single watermark value in the Turso config table (key 'claude-blog-last-seen-iso', category 'ops') so consecutive perch runs only surface posts newer than the last-seen date.",
+ "provider_scope": "turso-libsql-token (coarse; full DB access)"
+ },
+ {
+ "resource": "net.outbound",
+ "actions": [
+ "read",
+ "write"
+ ],
+ "rationale": "Talks to the configured Turso libSQL host for watermark state. No direct HTTP fetches against claude.com or any caller-supplied URL — fetching is the caller's responsibility via web_fetch.",
+ "provider_scope": "*.turso.io"
+ }
+ ],
+ "actions": [
+ {
+ "name": "parse_claude_blog",
+ "summary": "Extract blog posts from the rendered content of claude.com/blog. Pure parse — no I/O.",
+ "description": "Regex-based extractor that anchors on links to /blog/, walks the surrounding 600-char window backwards to find the nearest date (Month D, YYYY), and matches standalone category labels against a known list. Returns post dicts de-duplicated by URL, sorted newest-first by date, filtered to those with a parseable date.",
+ "docs": {
+ "goal": "Parse a fetched copy of claude.com/blog into structured post records.",
+ "inputs_brief": "content (req: rendered blog-index page; markdown or HTML — the regex shape matches either)",
+ "outputs_brief": "{posts: [{url, title, date (YYYY-MM-DD), category}]}",
+ "errors_brief": "(none — returns an empty list on parse miss)",
+ "example": "parse_claude_blog content='...blog page text...'"
+ },
+ "invocation": {
+ "kind": "stdin-json",
+ "argv_template": [
+ "parse-claude-blog"
+ ]
+ },
+ "input": {
+ "type": "object",
+ "required": [
+ "content"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "content": {
+ "type": "string"
+ }
+ }
+ },
+ "output": {
+ "format": "json",
+ "schema": {
+ "type": "object",
+ "required": [
+ "posts"
+ ],
+ "properties": {
+ "posts": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": [
+ "url",
+ "title",
+ "date"
+ ],
+ "properties": {
+ "url": {
+ "type": "string",
+ "format": "uri"
+ },
+ "title": {
+ "type": "string"
+ },
+ "date": {
+ "type": "string",
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}$"
+ },
+ "category": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "side_effects": "none",
+ "idempotent": true,
+ "scopes_used": [],
+ "error_envelope": "standard",
+ "runtime_telemetry": {}
+ },
+ {
+ "name": "filter_new",
+ "summary": "Return posts strictly newer than the watermark. Read-only, pure compute.",
+ "description": "Drops posts whose ISO date is <= last_seen. When last_seen is null (first run) returns an empty list — the seed run does not alert on historical content; the caller still advances set_last_seen so the next run alerts on anything published in between.",
+ "docs": {
+ "goal": "Filter parsed posts down to those newer than the watermark.",
+ "inputs_brief": "posts (req: from parse_claude_blog), last_seen (ISO date or null)",
+ "outputs_brief": "{new_posts: [...]}",
+ "errors_brief": "(none — pure compute)",
+ "example": "filter_new posts=[...] last_seen='2026-05-20'"
+ },
+ "invocation": {
+ "kind": "stdin-json",
+ "argv_template": [
+ "filter-new"
+ ]
+ },
+ "input": {
+ "type": "object",
+ "required": [
+ "posts"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "posts": {
+ "type": "array"
+ },
+ "last_seen": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "default": null
+ }
+ }
+ },
+ "output": {
+ "format": "json",
+ "schema": {
+ "type": "object",
+ "required": [
+ "new_posts"
+ ],
+ "properties": {
+ "new_posts": {
+ "type": "array"
+ }
+ }
+ }
+ },
+ "side_effects": "none",
+ "idempotent": true,
+ "scopes_used": [],
+ "error_envelope": "standard",
+ "runtime_telemetry": {}
+ },
+ {
+ "name": "get_last_seen",
+ "summary": "Read the last-seen ISO date watermark from Turso config.",
+ "description": "Calls scripts.config_get('claude-blog-last-seen-iso'). Returns the stored ISO date, or null when the key is unset (first run).",
+ "docs": {
+ "goal": "Fetch the watermark for the next perch run.",
+ "inputs_brief": "key (optional, default 'claude-blog-last-seen-iso')",
+ "outputs_brief": "{last_seen: string|null}",
+ "errors_brief": "tracking_unconfigured",
+ "example": "get_last_seen"
+ },
+ "invocation": {
+ "kind": "stdin-json",
+ "argv_template": [
+ "get-last-seen"
+ ]
+ },
+ "input": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "key": {
+ "type": "string",
+ "default": "claude-blog-last-seen-iso"
+ }
+ }
+ },
+ "output": {
+ "format": "json",
+ "schema": {
+ "type": "object",
+ "required": [
+ "last_seen"
+ ],
+ "properties": {
+ "last_seen": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ }
+ }
+ },
+ "side_effects": "read",
+ "idempotent": true,
+ "scopes_used": [
+ "memory.tracking",
+ "net.outbound"
+ ],
+ "error_envelope": "standard",
+ "runtime_telemetry": {}
+ },
+ {
+ "name": "set_last_seen",
+ "summary": "Write the last-seen ISO date watermark to Turso config under category 'ops'.",
+ "description": "Calls scripts.config_set(key, iso, 'ops'). Idempotent at the storage layer (re-setting to the same value is a no-op semantically; the underlying upsert touches the row).",
+ "docs": {
+ "goal": "Advance the watermark to the supplied ISO date.",
+ "inputs_brief": "iso (req: YYYY-MM-DD), key (optional, default 'claude-blog-last-seen-iso')",
+ "outputs_brief": "{stored: bool}",
+ "errors_brief": "tracking_unconfigured, iso_invalid",
+ "example": "set_last_seen iso='2026-05-27'"
+ },
+ "invocation": {
+ "kind": "stdin-json",
+ "argv_template": [
+ "set-last-seen"
+ ]
+ },
+ "input": {
+ "type": "object",
+ "required": [
+ "iso"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "iso": {
+ "type": "string",
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}$"
+ },
+ "key": {
+ "type": "string",
+ "default": "claude-blog-last-seen-iso"
+ }
+ }
+ },
+ "output": {
+ "format": "json",
+ "schema": {
+ "type": "object",
+ "required": [
+ "stored"
+ ],
+ "properties": {
+ "stored": {
+ "type": "boolean"
+ }
+ }
+ }
+ },
+ "side_effects": "write",
+ "idempotent": true,
+ "scopes_used": [
+ "memory.tracking",
+ "net.outbound"
+ ],
+ "error_envelope": "standard",
+ "runtime_telemetry": {}
+ },
+ {
+ "name": "format_for_report",
+ "summary": "Render new posts as HTML rows for the perch report. Pure compute, read-only.",
+ "description": "Returns inner HTML (no wrapper) so callers can omit the entire section when no posts are new — silence is the signal. Each row is - title (category) — date
; categories are omitted when null. HTML-escapes title and category text.",
+ "docs": {
+ "goal": "Format filtered posts for the perch HTML report.",
+ "inputs_brief": "new_posts (req: list from filter_new)",
+ "outputs_brief": "{html: string}",
+ "errors_brief": "(none — pure compute)",
+ "example": "format_for_report new_posts=[{url, title, date, category}]"
+ },
+ "invocation": {
+ "kind": "stdin-json",
+ "argv_template": [
+ "format-for-report"
+ ]
+ },
+ "input": {
+ "type": "object",
+ "required": [
+ "new_posts"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "new_posts": {
+ "type": "array"
+ }
+ }
+ },
+ "output": {
+ "format": "json",
+ "schema": {
+ "type": "object",
+ "required": [
+ "html"
+ ],
+ "properties": {
+ "html": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "side_effects": "none",
+ "idempotent": true,
+ "scopes_used": [],
+ "error_envelope": "standard",
+ "runtime_telemetry": {}
+ }
+ ],
+ "data_boundary": {
+ "reads": [
+ {
+ "resource": "memory.tracking",
+ "sensitivity": "low"
+ }
+ ],
+ "transmits": [],
+ "persists": [
+ {
+ "where": "tool_local",
+ "fields": [
+ "watermark.iso_date"
+ ]
+ }
+ ],
+ "retention": {
+ "tool_local_days": 365
+ }
+ },
+ "smoke": {
+ "kind": "shell",
+ "command": [
+ "python",
+ "-c",
+ "from muninn_utils.news_watch import parse_claude_blog, filter_new, format_for_report\nposts = parse_claude_blog('[Test post](https://claude.com/blog/test-post) May 10, 2026')\nassert isinstance(posts, list)\nnew = filter_new(posts, last_seen='2026-04-01')\nassert format_for_report(new) == '' or '- ' in format_for_report(new)\nprint('OK: news_watch public surface present')\n"
+ ],
+ "timeout_seconds": 5,
+ "success": {
+ "exit_code": 0,
+ "stdout_regex": "^OK: news_watch public surface present$"
+ }
+ },
+ "kill_switch": {
+ "kind": "manual",
+ "instructions_url": "https://github.com/oaustegard/muninn-utilities/blob/main/manifests/news-watch/REVOKE.md"
+ },
+ "cost": {
+ "install_fee_cents": 0,
+ "monthly_fee_cents": 0,
+ "usage_model": "none"
+ },
+ "support": {
+ "issues_url": "https://github.com/oaustegard/muninn-utilities/issues",
+ "docs_url": "https://github.com/oaustegard/muninn-utilities/blob/main/muninn_utils/news_watch.py"
+ }
+}
diff --git a/mirrors/manifests/muninn-perch-publish.json b/mirrors/manifests/muninn-perch-publish.json
new file mode 100644
index 0000000..b867ae8
--- /dev/null
+++ b/mirrors/manifests/muninn-perch-publish.json
@@ -0,0 +1,208 @@
+{
+ "manifest_version": "0.4",
+ "tool": {
+ "id": "muninn-perch-publish",
+ "version": "0.1.0",
+ "name": "Muninn perch_publish",
+ "summary": "Publish a perch flight log (GitHub discussion) to muninn.austegard.com/perch/ as HTML, updating the perch index and Atom feed.",
+ "description": "Fetches a discussion via the GitHub GraphQL API, converts the markdown body to HTML, slugifies the title, and commits three files in one batch (the rendered page under perch/, the regenerated index.html, the regenerated feed.xml). Reads existing entries from the live site to merge the new one into the index/feed without re-fetching every prior discussion. Same primary credential as `blog_publish` and `issue_close` (GH_TOKEN), and the GH_TOKEN must have access to BOTH oaustegard/claude-skills (read discussions) and oaustegard/muninn.austegard.com (write commits).",
+ "homepage": "https://github.com/oaustegard/muninn-utilities/blob/main/muninn_utils/perch_publish.py",
+ "author": {
+ "name": "Muninn (raven of memory; agent operating on behalf of Oskar Austegard)",
+ "url": "https://muninn.austegard.com"
+ },
+ "license": "MIT",
+ "tags": [
+ "publishing",
+ "github-pages",
+ "atom-feed",
+ "perch",
+ "discussions"
+ ]
+ },
+ "runtime": {
+ "kind": "python-module",
+ "install": {
+ "method": "preinstalled",
+ "locator": {
+ "kind": "python-module",
+ "module": "muninn_utils.perch_publish"
+ }
+ },
+ "entrypoint": {
+ "command": [
+ "python",
+ "-m",
+ "muninn_utils.perch_publish"
+ ]
+ }
+ },
+ "env": [
+ {
+ "name": "GH_TOKEN",
+ "prompt": "GitHub personal access token. Needs read access to oaustegard/claude-skills (to fetch the discussion via GraphQL) AND write access to oaustegard/muninn.austegard.com (to commit the rendered page, index, and feed). Same coarse credential as blog_publish, issue_close, perch_triage, verify_patch \u2014 share-by-default is intentional. Falls back to GITHUB_TOKEN if GH_TOKEN is not set.",
+ "secret": true,
+ "required": true,
+ "validation_regex": "^(ghp_[A-Za-z0-9]{36}|github_pat_[A-Za-z0-9_]+)$",
+ "obtain_url": "https://github.com/settings/personal-access-tokens"
+ },
+ {
+ "name": "GITHUB_TOKEN",
+ "prompt": "Optional fallback for GH_TOKEN. The source reads `os.environ.get('GH_TOKEN') or os.environ.get('GITHUB_TOKEN')`.",
+ "secret": true,
+ "required": false,
+ "validation_regex": "^(ghp_[A-Za-z0-9]{36}|github_pat_[A-Za-z0-9_]+)$",
+ "obtain_url": "https://github.com/settings/personal-access-tokens"
+ }
+ ],
+ "scopes": [
+ {
+ "resource": "github.repo.contents",
+ "actions": [
+ "read",
+ "write"
+ ],
+ "rationale": "Reads existing perch entries from oaustegard/muninn.austegard.com to rebuild the index and feed; writes the new page, the updated index, and the updated feed in a single commit. Lands directly on main with no PR.",
+ "provider_scope": "github-pat (coarse; full account write access)"
+ },
+ {
+ "resource": "github.discussions",
+ "actions": [
+ "read"
+ ],
+ "rationale": "Fetches the discussion body and metadata from oaustegard/claude-skills via the GraphQL API.",
+ "provider_scope": "github-pat (coarse)"
+ },
+ {
+ "resource": "net.outbound",
+ "actions": [
+ "read",
+ "write"
+ ],
+ "rationale": "api.github.com for GraphQL discussion read and REST commits; raw.githubusercontent.com (or api.github.com/contents) for reading existing perch index entries.",
+ "provider_scope": "api.github.com, raw.githubusercontent.com, muninn.austegard.com"
+ }
+ ],
+ "actions": [
+ {
+ "name": "publish_flight_log",
+ "summary": "Render a GitHub discussion as a perch page on muninn.austegard.com and update the perch index + Atom feed.",
+ "description": "Fetches discussion #number from oaustegard/claude-skills, slugifies the title, converts markdown to HTML, reads the existing perch index to compute the new entry list, and commits three files (page, index, feed) in one commit on the publish-target repo's default branch.",
+ "docs": {
+ "goal": "Publish a perch flight log discussion as a public HTML page.",
+ "inputs_brief": "number (req: discussion #), repo (default oaustegard/muninn.austegard.com)",
+ "outputs_brief": "{url, slug, commit_sha}",
+ "errors_brief": "auth_invalid, discussion_not_found, commit_failed",
+ "example": "publish_flight_log number=430"
+ },
+ "invocation": {
+ "kind": "stdin-json",
+ "argv_template": [
+ "publish-flight-log"
+ ]
+ },
+ "input": {
+ "type": "object",
+ "required": [
+ "number"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "number": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "repo": {
+ "type": "string",
+ "pattern": "^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$",
+ "default": "oaustegard/muninn.austegard.com"
+ }
+ }
+ },
+ "output": {
+ "format": "json",
+ "schema": {
+ "type": "object",
+ "required": [
+ "url",
+ "slug",
+ "commit_sha"
+ ],
+ "properties": {
+ "url": {
+ "type": "string",
+ "format": "uri"
+ },
+ "slug": {
+ "type": "string"
+ },
+ "commit_sha": {
+ "type": "string",
+ "pattern": "^[0-9a-f]{40}$"
+ }
+ }
+ }
+ },
+ "side_effects": "destructive",
+ "idempotent": false,
+ "scopes_used": [
+ "github.repo.contents",
+ "github.discussions",
+ "net.outbound"
+ ],
+ "error_envelope": "standard",
+ "runtime_telemetry": {}
+ }
+ ],
+ "data_boundary": {
+ "reads": [
+ {
+ "resource": "github.discussions",
+ "sensitivity": "low"
+ },
+ {
+ "resource": "github.repo.contents",
+ "sensitivity": "low"
+ }
+ ],
+ "transmits": [
+ {
+ "to": "api.github.com",
+ "fields": [
+ "env.GH_TOKEN",
+ "input.number"
+ ],
+ "purpose": "fetch discussion body (read) and commit rendered page + index + feed (write)",
+ "third_party_retention": "none-per-vendor-tos",
+ "vendor_tos_url": "https://docs.github.com/en/site-policy/privacy-policies/github-general-privacy-statement"
+ }
+ ],
+ "persists": []
+ },
+ "smoke": {
+ "kind": "shell",
+ "command": [
+ "python",
+ "-c",
+ "import os, json, urllib.request\ntoken = os.environ.get('GH_TOKEN') or os.environ.get('GITHUB_TOKEN')\nassert token, 'no GH_TOKEN / GITHUB_TOKEN set'\nreq = urllib.request.Request('https://api.github.com/repos/oaustegard/muninn.austegard.com', headers={'Authorization': f'token {token}', 'Accept': 'application/vnd.github+json', 'User-Agent': 'perch-publish-smoke'})\nd = json.loads(urllib.request.urlopen(req).read())\nassert d['permissions']['push'], 'token lacks write access to publish-target repo'\nprint('OK: write access confirmed to', d['name'])\n"
+ ],
+ "timeout_seconds": 10,
+ "success": {
+ "exit_code": 0,
+ "stdout_regex": "OK: write access confirmed to muninn\\.austegard\\.com"
+ }
+ },
+ "kill_switch": {
+ "kind": "manual",
+ "instructions_url": "https://github.com/oaustegard/muninn-utilities/blob/main/manifests/perch-publish/REVOKE.md"
+ },
+ "cost": {
+ "install_fee_cents": 0,
+ "monthly_fee_cents": 0,
+ "usage_model": "external"
+ },
+ "support": {
+ "issues_url": "https://github.com/oaustegard/muninn-utilities/issues",
+ "docs_url": "https://github.com/oaustegard/muninn-utilities/blob/main/muninn_utils/perch_publish.py"
+ }
+}
diff --git a/mirrors/manifests/muninn-perch-triage.json b/mirrors/manifests/muninn-perch-triage.json
new file mode 100644
index 0000000..84474b9
--- /dev/null
+++ b/mirrors/manifests/muninn-perch-triage.json
@@ -0,0 +1,312 @@
+{
+ "manifest_version": "0.4",
+ "tool": {
+ "id": "muninn-perch-triage",
+ "version": "0.1.0",
+ "name": "Muninn perch_triage",
+ "summary": "Triage open Perch flight-log discussions by reaction signal. Groups them into action buckets (auto-close, discuss, file-issue, hold, correct, nag). Optional auto-close path executes the THUMBS_UP/LAUGH bucket.",
+ "description": "Two operating modes. **Recommendation-only** (auto_close=false): reads open Flight Log discussions on oaustegard/claude-skills, classifies each by primary reaction (priority order: ROCKET > HEART > CONFUSED > EYES > HOORAY > THUMBS_DOWN > THUMBS_UP > LAUGH), groups them into action buckets, and returns the report. Pure read; no GitHub writes, no memory writes. **Auto-close** (auto_close=true, default): for THUMBS_UP / LAUGH reactions only, additionally writes a `world`-typed memory summarising the log, then closes the GH discussion with a comment referencing the memory id. Issue #5 calls this out as the test for a tool that produces a recommendation rather than a change \u2014 the recommendation-only mode is read-only with no writes; the auto-close mode is write-and-destructive on a narrow reaction set.",
+ "homepage": "https://github.com/oaustegard/muninn-utilities/blob/main/muninn_utils/perch_triage.py",
+ "author": {
+ "name": "Muninn (raven of memory; agent operating on behalf of Oskar Austegard)",
+ "url": "https://muninn.austegard.com"
+ },
+ "license": "MIT",
+ "tags": [
+ "github",
+ "discussions",
+ "triage",
+ "flight-logs",
+ "reactions"
+ ]
+ },
+ "runtime": {
+ "kind": "python-module",
+ "install": {
+ "method": "preinstalled",
+ "locator": {
+ "kind": "python-module",
+ "module": "muninn_utils.perch_triage"
+ }
+ },
+ "entrypoint": {
+ "command": [
+ "python",
+ "-m",
+ "muninn_utils.perch_triage"
+ ]
+ }
+ },
+ "env": [
+ {
+ "name": "GH_TOKEN",
+ "prompt": "GitHub personal access token. Reads discussion content and reactions; writes only when auto_close=true (closes the discussion with a comment). Same coarse credential as perch_publish, issue_close, blog_publish, verify_patch. Falls back to GITHUB_TOKEN.",
+ "secret": true,
+ "required": true,
+ "validation_regex": "^(ghp_[A-Za-z0-9]{36}|github_pat_[A-Za-z0-9_]+)$",
+ "obtain_url": "https://github.com/settings/personal-access-tokens"
+ },
+ {
+ "name": "GITHUB_TOKEN",
+ "prompt": "Optional fallback for GH_TOKEN. The source reads `os.environ.get('GH_TOKEN') or os.environ.get('GITHUB_TOKEN')`.",
+ "secret": true,
+ "required": false,
+ "validation_regex": "^(ghp_[A-Za-z0-9]{36}|github_pat_[A-Za-z0-9_]+)$",
+ "obtain_url": "https://github.com/settings/personal-access-tokens"
+ },
+ {
+ "name": "TURSO_TOKEN",
+ "prompt": "Turso libSQL auth token for the Muninn memory database. Required only if you intend to use auto_close=true; the recommendation-only path does not write memories. Optional.",
+ "secret": true,
+ "required": false,
+ "obtain_url": "https://app.turso.tech/"
+ },
+ {
+ "name": "TURSO_URL",
+ "prompt": "Hostname of the Muninn memory libSQL database. Same conditional rule as TURSO_TOKEN.",
+ "secret": false,
+ "required": false,
+ "validation_regex": "^[a-z0-9-]+\\.[a-z0-9-]+\\.turso\\.io$"
+ }
+ ],
+ "scopes": [
+ {
+ "resource": "github.discussions",
+ "actions": [
+ "read",
+ "write"
+ ],
+ "rationale": "Reads open Flight Log discussions and their reactions. When auto_close=true and a discussion has a THUMBS_UP/LAUGH reaction, writes a closing comment and toggles state to CLOSED.",
+ "provider_scope": "github-pat (coarse; full account write access)"
+ },
+ {
+ "resource": "memory.tracking",
+ "actions": [
+ "write"
+ ],
+ "rationale": "When auto_close=true, writes a `world`-typed memory summarising each auto-closed flight log so the closure remains traceable. Read-only when auto_close=false.",
+ "provider_scope": "turso-libsql-token (coarse; full DB access)"
+ },
+ {
+ "resource": "net.outbound",
+ "actions": [
+ "read",
+ "write"
+ ],
+ "rationale": "Talks to api.github.com (GraphQL for discussion read, REST for close-with-comment) and to the configured Turso libSQL host when auto-closing.",
+ "provider_scope": "api.github.com, *.turso.io"
+ }
+ ],
+ "actions": [
+ {
+ "name": "triage",
+ "summary": "Read open Flight Log discussions and group them by reaction-driven action. Optionally auto-close the THUMBS_UP / LAUGH bucket.",
+ "description": "Synchronously: fetches open discussions in the Flight Logs category, classifies each by primary reaction, and returns the result grouped into action buckets (auto_closed, discuss_priority, file_issues, hold, correction, close_not_useful, close_celebrate, nag, unreacted_recent). When auto_close=true, the auto_closed bucket is realised \u2014 each log gets a memory write and a GH close-with-comment; the others are recommendations the caller must execute manually. The unreacted_recent / nag split surfaces logs older than nag_days that have not been reacted to yet.",
+ "docs": {
+ "goal": "Triage Perch flight logs by reaction; optionally auto-close the obvious-good bucket.",
+ "inputs_brief": "auto_close (bool, default true), nag_days (int, default 3), limit (int, default 25)",
+ "outputs_brief": "{auto_closed, discuss_priority, file_issues, hold, correction, close_not_useful, close_celebrate, nag, unreacted_recent} \u2014 each an array of log dicts",
+ "errors_brief": "auth_invalid, category_not_found, network_unreachable, tracking_unconfigured (when auto_close=true and TURSO_* not set)",
+ "example": "triage auto_close=false nag_days=3"
+ },
+ "invocation": {
+ "kind": "subcommand",
+ "argv_template": [
+ "triage",
+ "--auto-close",
+ "${input.auto_close}",
+ "--nag-days",
+ "${input.nag_days}",
+ "--limit",
+ "${input.limit}"
+ ]
+ },
+ "input": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "auto_close": {
+ "type": "boolean",
+ "default": true
+ },
+ "nag_days": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 30,
+ "default": 3
+ },
+ "limit": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 100,
+ "default": 25
+ }
+ }
+ },
+ "output": {
+ "format": "json",
+ "schema": {
+ "type": "object",
+ "required": [
+ "auto_closed",
+ "discuss_priority",
+ "file_issues",
+ "hold",
+ "correction",
+ "close_not_useful",
+ "close_celebrate",
+ "nag",
+ "unreacted_recent"
+ ],
+ "properties": {
+ "auto_closed": {
+ "type": "array"
+ },
+ "discuss_priority": {
+ "type": "array"
+ },
+ "file_issues": {
+ "type": "array"
+ },
+ "hold": {
+ "type": "array"
+ },
+ "correction": {
+ "type": "array"
+ },
+ "close_not_useful": {
+ "type": "array"
+ },
+ "close_celebrate": {
+ "type": "array"
+ },
+ "nag": {
+ "type": "array"
+ },
+ "unreacted_recent": {
+ "type": "array"
+ }
+ }
+ }
+ },
+ "side_effects": "destructive",
+ "idempotent": false,
+ "scopes_used": [
+ "github.discussions",
+ "memory.tracking",
+ "net.outbound"
+ ],
+ "error_envelope": "standard",
+ "runtime_telemetry": {}
+ },
+ {
+ "name": "triage_report",
+ "summary": "Format a triage result as a concise human-readable report. Read-only.",
+ "description": "Pure formatting wrapper around `triage`. If no result is supplied, calls `triage` with default args and formats the result. Returns a markdown-flavored multi-line string organised by action bucket. Read-only when called with a precomputed result; calls through to `triage` (with its destructive side-effects) when result=null.",
+ "docs": {
+ "goal": "Render a triage result as text.",
+ "inputs_brief": "result (optional triage output dict)",
+ "outputs_brief": "{report: string}",
+ "errors_brief": "(inherits from triage when result=null)",
+ "example": "triage_report"
+ },
+ "invocation": {
+ "kind": "stdin-json",
+ "argv_template": [
+ "triage-report"
+ ]
+ },
+ "input": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "result": {
+ "type": [
+ "object",
+ "null"
+ ],
+ "default": null
+ }
+ }
+ },
+ "output": {
+ "format": "json",
+ "schema": {
+ "type": "object",
+ "required": [
+ "report"
+ ],
+ "properties": {
+ "report": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "side_effects": "read",
+ "idempotent": true,
+ "scopes_used": [
+ "github.discussions"
+ ],
+ "error_envelope": "standard",
+ "runtime_telemetry": {}
+ }
+ ],
+ "data_boundary": {
+ "reads": [
+ {
+ "resource": "github.discussions",
+ "sensitivity": "low"
+ }
+ ],
+ "transmits": [
+ {
+ "to": "api.github.com",
+ "fields": [
+ "env.GH_TOKEN"
+ ],
+ "purpose": "discussion read + auto-close write",
+ "third_party_retention": "none-per-vendor-tos",
+ "vendor_tos_url": "https://docs.github.com/en/site-policy/privacy-policies/github-general-privacy-statement"
+ }
+ ],
+ "persists": [
+ {
+ "where": "tool_local",
+ "fields": [
+ "log_summary",
+ "log_number"
+ ]
+ }
+ ],
+ "retention": {
+ "tool_local_days": 365
+ }
+ },
+ "smoke": {
+ "kind": "shell",
+ "command": [
+ "python",
+ "-c",
+ "from muninn_utils.perch_triage import fetch_open_logs\nlogs = fetch_open_logs(limit=1)\nassert isinstance(logs, list)\nprint('OK: fetched', len(logs), 'logs')\n"
+ ],
+ "timeout_seconds": 15,
+ "success": {
+ "exit_code": 0,
+ "stdout_regex": "^OK: fetched [0-9]+ logs$"
+ }
+ },
+ "kill_switch": {
+ "kind": "manual",
+ "instructions_url": "https://github.com/oaustegard/muninn-utilities/blob/main/manifests/perch-triage/REVOKE.md"
+ },
+ "cost": {
+ "install_fee_cents": 0,
+ "monthly_fee_cents": 0,
+ "usage_model": "external"
+ },
+ "support": {
+ "issues_url": "https://github.com/oaustegard/muninn-utilities/issues",
+ "docs_url": "https://github.com/oaustegard/muninn-utilities/blob/main/muninn_utils/perch_triage.py"
+ }
+}
diff --git a/mirrors/manifests/muninn-remind.json b/mirrors/manifests/muninn-remind.json
new file mode 100644
index 0000000..2d68d7e
--- /dev/null
+++ b/mirrors/manifests/muninn-remind.json
@@ -0,0 +1,557 @@
+{
+ "manifest_version": "0.4",
+ "tool": {
+ "id": "muninn-remind",
+ "version": "0.1.0",
+ "name": "Muninn remind",
+ "summary": "Reminder system over the Muninn memory store. Create one-shot or recurring reminders, mark them done, snooze them, sweep stale ones. All persistence rides on the same Turso DB as the rest of Muninn's memory subsystem.",
+ "description": "Six operations: remind, remind_done, remind_snooze, remind_due, remind_list, remind_sweep. Each reminder is a `procedure`-typed memory tagged `remind`, `remind-active`, and `remind-`. The reminder's due time is encoded in the memory's `valid_from` field; recurring reminders update `valid_from` forward when completed; nag-kind reminders re-surface every boot until done; notice-kind auto-resolve after first surface. The tool is a thin write-and-tag layer over the `remembering` skill \u2014 every persistent operation is a `_remember` / `_exec` call against Turso. Issue #5 calls this out as the test for a tool that writes to the user's external reminder system; v0.4 `writes[]` would distinguish this from third-party transmits the way it would for blog_publish.",
+ "homepage": "https://github.com/oaustegard/muninn-utilities/blob/main/muninn_utils/remind.py",
+ "author": {
+ "name": "Muninn (raven of memory; agent operating on behalf of Oskar Austegard)",
+ "url": "https://muninn.austegard.com"
+ },
+ "license": "MIT",
+ "tags": [
+ "reminders",
+ "memory",
+ "scheduling",
+ "recurring",
+ "muninn-internal"
+ ]
+ },
+ "runtime": {
+ "kind": "python-module",
+ "install": {
+ "method": "preinstalled",
+ "locator": {
+ "kind": "python-module",
+ "module": "muninn_utils.remind"
+ }
+ },
+ "entrypoint": {
+ "command": [
+ "python",
+ "-m",
+ "muninn_utils.remind"
+ ]
+ }
+ },
+ "env": [
+ {
+ "name": "TURSO_TOKEN",
+ "prompt": "Turso libSQL auth token for the Muninn memory database. The reminder primitives all write through the `remembering` skill, which uses these credentials. Required.",
+ "secret": true,
+ "required": true,
+ "obtain_url": "https://app.turso.tech/"
+ },
+ {
+ "name": "TURSO_URL",
+ "prompt": "Hostname of the Muninn memory libSQL database, e.g. 'mydb-username.turso.io'.",
+ "secret": false,
+ "required": true,
+ "validation_regex": "^[a-z0-9-]+\\.[a-z0-9-]+\\.turso\\.io$"
+ }
+ ],
+ "scopes": [
+ {
+ "resource": "memory.tracking",
+ "actions": [
+ "read",
+ "write"
+ ],
+ "rationale": "Reminders are stored as `procedure`-typed memories with structured tags and `valid_from` due times. All six operations read and/or write this surface.",
+ "provider_scope": "turso-libsql-token (coarse; full DB access)"
+ },
+ {
+ "resource": "net.outbound",
+ "actions": [
+ "read",
+ "write"
+ ],
+ "rationale": "Talks to the configured Turso libSQL host. No other outbound destinations.",
+ "provider_scope": "*.turso.io"
+ }
+ ],
+ "actions": [
+ {
+ "name": "create",
+ "summary": "Create a reminder. Optionally recurring, optionally with an early-alert window, optionally tagged.",
+ "description": "Stores a `procedure`-typed memory with the reminder text, due time, and metadata (kind, recur_days, alert_before_days). `kind=nag` re-surfaces every boot until completed; `kind=notice` resolves after first surface. `recur_days` makes the reminder repeat \u2014 completing rolls `valid_from` forward by that many days rather than archiving. `alert_before_days` makes the reminder visible N days before due. Returns the memory id.",
+ "docs": {
+ "goal": "Create a reminder.",
+ "inputs_brief": "what (req), due (ISO or shorthand like '+3d'), kind (nag|notice), recur_days, alert_before_days, tags, priority",
+ "outputs_brief": "{memory_id}",
+ "errors_brief": "tracking_unconfigured, due_unparseable",
+ "example": "create what='check verify_patch tracking review' due='+7d' kind='notice'"
+ },
+ "invocation": {
+ "kind": "stdin-json",
+ "argv_template": [
+ "create"
+ ]
+ },
+ "input": {
+ "type": "object",
+ "required": [
+ "what"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "what": {
+ "type": "string",
+ "minLength": 1
+ },
+ "due": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "default": null
+ },
+ "kind": {
+ "type": "string",
+ "enum": [
+ "nag",
+ "notice"
+ ],
+ "default": "nag"
+ },
+ "recur_days": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1,
+ "default": null
+ },
+ "alert_before_days": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 0,
+ "default": null
+ },
+ "tags": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "default": []
+ },
+ "priority": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 5,
+ "default": 1
+ }
+ }
+ },
+ "output": {
+ "format": "json",
+ "schema": {
+ "type": "object",
+ "required": [
+ "memory_id"
+ ],
+ "properties": {
+ "memory_id": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "side_effects": "write",
+ "idempotent": false,
+ "scopes_used": [
+ "memory.tracking",
+ "net.outbound"
+ ],
+ "error_envelope": "standard",
+ "runtime_telemetry": {}
+ },
+ {
+ "name": "done",
+ "summary": "Mark a reminder complete. Recurring reminders roll `valid_from` forward; one-shot reminders move to done.",
+ "description": "Looks up the reminder by full id or 8-char prefix. Recurring (`recur_days` set): updates the memory's `valid_from` by recur_days and keeps the `remind-active` tag. One-shot: drops the `remind-active` tag and adds `remind-done`. Idempotent in the one-shot case (re-completing is a no-op).",
+ "docs": {
+ "goal": "Complete a reminder.",
+ "inputs_brief": "reminder_id (full or 8-char prefix), note (optional)",
+ "outputs_brief": "{status: string, next_due: string|null}",
+ "errors_brief": "tracking_unconfigured, reminder_not_found",
+ "example": "done reminder_id='abc12345' note='shipped'"
+ },
+ "invocation": {
+ "kind": "stdin-json",
+ "argv_template": [
+ "done"
+ ]
+ },
+ "input": {
+ "type": "object",
+ "required": [
+ "reminder_id"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "reminder_id": {
+ "type": "string",
+ "minLength": 1
+ },
+ "note": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "default": null
+ }
+ }
+ },
+ "output": {
+ "format": "json",
+ "schema": {
+ "type": "object",
+ "required": [
+ "status"
+ ],
+ "properties": {
+ "status": {
+ "type": "string"
+ },
+ "next_due": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ }
+ }
+ },
+ "side_effects": "write",
+ "idempotent": false,
+ "scopes_used": [
+ "memory.tracking",
+ "net.outbound"
+ ],
+ "error_envelope": "standard",
+ "runtime_telemetry": {}
+ },
+ {
+ "name": "snooze",
+ "summary": "Push a reminder's due time forward.",
+ "description": "Updates the reminder's `valid_from` to the supplied ISO datetime or relative shorthand. Does not change the active/done status; the reminder simply re-becomes visible at the new time.",
+ "docs": {
+ "goal": "Defer a reminder.",
+ "inputs_brief": "reminder_id, until (ISO or shorthand)",
+ "outputs_brief": "{status, new_due}",
+ "errors_brief": "tracking_unconfigured, reminder_not_found, until_unparseable",
+ "example": "snooze reminder_id='abc12345' until='+1w'"
+ },
+ "invocation": {
+ "kind": "stdin-json",
+ "argv_template": [
+ "snooze"
+ ]
+ },
+ "input": {
+ "type": "object",
+ "required": [
+ "reminder_id",
+ "until"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "reminder_id": {
+ "type": "string",
+ "minLength": 1
+ },
+ "until": {
+ "type": "string",
+ "minLength": 1
+ }
+ }
+ },
+ "output": {
+ "format": "json",
+ "schema": {
+ "type": "object",
+ "required": [
+ "status",
+ "new_due"
+ ],
+ "properties": {
+ "status": {
+ "type": "string"
+ },
+ "new_due": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "side_effects": "write",
+ "idempotent": true,
+ "scopes_used": [
+ "memory.tracking",
+ "net.outbound"
+ ],
+ "error_envelope": "standard",
+ "runtime_telemetry": {}
+ },
+ {
+ "name": "due",
+ "summary": "List active reminders due within the supplied horizon. Read-only.",
+ "description": "Returns active (not-done) reminders whose `valid_from` is within `horizon_days` from now (or earlier \u2014 overdue reminders surface here too). Each result includes the full memory id, the reminder text, the due time, and the kind.",
+ "docs": {
+ "goal": "Show active reminders due soon (or overdue).",
+ "inputs_brief": "horizon_days (int, default 2)",
+ "outputs_brief": "{reminders: [{id, what, due, kind, age_days}]}",
+ "errors_brief": "tracking_unconfigured",
+ "example": "due horizon_days=7"
+ },
+ "invocation": {
+ "kind": "subcommand",
+ "argv_template": [
+ "due",
+ "--horizon-days",
+ "${input.horizon_days}"
+ ]
+ },
+ "input": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "horizon_days": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 365,
+ "default": 2
+ }
+ }
+ },
+ "output": {
+ "format": "json",
+ "schema": {
+ "type": "object",
+ "required": [
+ "reminders"
+ ],
+ "properties": {
+ "reminders": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "what": {
+ "type": "string"
+ },
+ "due": {
+ "type": "string"
+ },
+ "kind": {
+ "type": "string"
+ },
+ "age_days": {
+ "type": "integer"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "side_effects": "read",
+ "idempotent": true,
+ "scopes_used": [
+ "memory.tracking",
+ "net.outbound"
+ ],
+ "error_envelope": "standard",
+ "runtime_telemetry": {}
+ },
+ {
+ "name": "list",
+ "summary": "List all reminders. Read-only.",
+ "description": "Returns all reminders, optionally including done ones. Useful for full-state inspection.",
+ "docs": {
+ "goal": "Enumerate reminders.",
+ "inputs_brief": "include_done (bool, default false)",
+ "outputs_brief": "{reminders: [...]}",
+ "errors_brief": "tracking_unconfigured",
+ "example": "list include_done=true"
+ },
+ "invocation": {
+ "kind": "subcommand",
+ "argv_template": [
+ "list",
+ "--include-done",
+ "${input.include_done}"
+ ]
+ },
+ "input": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "include_done": {
+ "type": "boolean",
+ "default": false
+ }
+ }
+ },
+ "output": {
+ "format": "json",
+ "schema": {
+ "type": "object",
+ "required": [
+ "reminders"
+ ],
+ "properties": {
+ "reminders": {
+ "type": "array"
+ }
+ }
+ }
+ },
+ "side_effects": "read",
+ "idempotent": true,
+ "scopes_used": [
+ "memory.tracking",
+ "net.outbound"
+ ],
+ "error_envelope": "standard",
+ "runtime_telemetry": {}
+ },
+ {
+ "name": "sweep",
+ "summary": "Archive long-stale reminders. Defaults to dry-run.",
+ "description": "Finds reminders whose `valid_from` is older than `archive_after_days`, or recurring reminders that have missed `missed_cycles` consecutive due times, and archives them (drops `remind-active`, adds `remind-archived`). `dry_run=true` (default) returns the would-archive list without acting.",
+ "docs": {
+ "goal": "Clean up stale reminders.",
+ "inputs_brief": "archive_after_days (int, default 21), missed_cycles (int, default 2), dry_run (bool, default true)",
+ "outputs_brief": "{archived: [...], count: int, dry_run: bool}",
+ "errors_brief": "tracking_unconfigured",
+ "example": "sweep dry_run=false"
+ },
+ "invocation": {
+ "kind": "subcommand",
+ "argv_template": [
+ "sweep",
+ "--archive-after-days",
+ "${input.archive_after_days}",
+ "--missed-cycles",
+ "${input.missed_cycles}",
+ "--dry-run",
+ "${input.dry_run}"
+ ]
+ },
+ "input": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "archive_after_days": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 365,
+ "default": 21
+ },
+ "missed_cycles": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 12,
+ "default": 2
+ },
+ "dry_run": {
+ "type": "boolean",
+ "default": true
+ }
+ }
+ },
+ "output": {
+ "format": "json",
+ "schema": {
+ "type": "object",
+ "required": [
+ "archived",
+ "count",
+ "dry_run"
+ ],
+ "properties": {
+ "archived": {
+ "type": "array"
+ },
+ "count": {
+ "type": "integer"
+ },
+ "dry_run": {
+ "type": "boolean"
+ }
+ }
+ }
+ },
+ "side_effects": "write",
+ "idempotent": false,
+ "scopes_used": [
+ "memory.tracking",
+ "net.outbound"
+ ],
+ "error_envelope": "standard",
+ "runtime_telemetry": {}
+ }
+ ],
+ "data_boundary": {
+ "reads": [
+ {
+ "resource": "memory.tracking",
+ "sensitivity": "medium"
+ }
+ ],
+ "transmits": [],
+ "persists": [
+ {
+ "where": "tool_local",
+ "fields": [
+ "what",
+ "due",
+ "kind",
+ "recur_days",
+ "alert_before_days",
+ "tags",
+ "priority"
+ ]
+ }
+ ],
+ "retention": {
+ "tool_local_days": 365
+ }
+ },
+ "smoke": {
+ "kind": "shell",
+ "command": [
+ "python",
+ "-c",
+ "from muninn_utils import remind\n# Smoke: import resolves and the public surface is callable.\nassert callable(remind.remind)\nassert callable(remind.remind_done)\nassert callable(remind.remind_due)\nprint('OK: remind public surface present')\n"
+ ],
+ "timeout_seconds": 5,
+ "success": {
+ "exit_code": 0,
+ "stdout_regex": "^OK: remind public surface present$"
+ }
+ },
+ "kill_switch": {
+ "kind": "manual",
+ "instructions_url": "https://github.com/oaustegard/muninn-utilities/blob/main/manifests/remind/REVOKE.md"
+ },
+ "cost": {
+ "install_fee_cents": 0,
+ "monthly_fee_cents": 0,
+ "usage_model": "none"
+ },
+ "support": {
+ "issues_url": "https://github.com/oaustegard/muninn-utilities/issues",
+ "docs_url": "https://github.com/oaustegard/muninn-utilities/blob/main/muninn_utils/remind.py"
+ }
+}
diff --git a/mirrors/manifests/muninn-task-policy.json b/mirrors/manifests/muninn-task-policy.json
new file mode 100644
index 0000000..29af732
--- /dev/null
+++ b/mirrors/manifests/muninn-task-policy.json
@@ -0,0 +1,295 @@
+{
+ "manifest_version": "0.4",
+ "tool": {
+ "id": "muninn-task-policy",
+ "version": "0.1.0",
+ "name": "Muninn task_policy",
+ "summary": "Load the live policy for a perch autonomous task. Reads the {task}-command ops entry, recent preference memories, and the most recent real run, so task prompts route to fresh policy rather than hardcoded behavior.",
+ "description": "Three loads, all routed through the `remembering` library against Turso: (1) the {task_name}-command ops entry via config_get, (2) the N most recent memories tagged BOTH the task name AND 'preference' via recall(), (3) the most recent memory tagged with the task name and 'perch-time' but NOT 'skip' via a direct _exec SQL. Returns a dict combining the three plus a days_since_last_run helper. Pure read — no writes. The utility itself has no direct outbound HTTP; all I/O is mediated by the remembering library, which talks to Turso. See oaustegard/muninn-utilities#14 for the architectural background.",
+ "homepage": "https://github.com/oaustegard/muninn-utilities/blob/main/muninn_utils/task_policy.py",
+ "author": {
+ "name": "Muninn (raven of memory; agent operating on behalf of Oskar Austegard)",
+ "url": "https://muninn.austegard.com"
+ },
+ "license": "MIT",
+ "tags": [
+ "policy",
+ "perch",
+ "config",
+ "memory",
+ "muninn-internal"
+ ]
+ },
+ "runtime": {
+ "kind": "python-module",
+ "install": {
+ "method": "preinstalled",
+ "locator": {
+ "kind": "python-module",
+ "module": "muninn_utils.task_policy"
+ }
+ },
+ "entrypoint": {
+ "command": [
+ "python",
+ "-m",
+ "muninn_utils.task_policy"
+ ]
+ }
+ },
+ "env": [
+ {
+ "name": "TURSO_TOKEN",
+ "prompt": "Turso libSQL auth token for the Muninn memory database. The three reads (config_get, recall, _exec) all route through the remembering library, which uses these credentials. Required.",
+ "secret": true,
+ "required": true,
+ "obtain_url": "https://app.turso.tech/"
+ },
+ {
+ "name": "TURSO_URL",
+ "prompt": "Hostname of the Muninn memory libSQL database, e.g. 'mydb-username.turso.io'.",
+ "secret": false,
+ "required": true,
+ "validation_regex": "^[a-z0-9-]+\\.[a-z0-9-]+\\.turso\\.io$"
+ }
+ ],
+ "scopes": [
+ {
+ "resource": "memory.tracking",
+ "actions": [
+ "read"
+ ],
+ "rationale": "Reads the {task_name}-command ops entry, preference-tagged memories, and the most recent perch-time run for the named task. Never writes — the utility is a read-only policy resolver.",
+ "provider_scope": "turso-libsql-token (coarse; full DB access)"
+ },
+ {
+ "resource": "net.outbound",
+ "actions": [
+ "read"
+ ],
+ "rationale": "Talks to the configured Turso libSQL host via the remembering library. No other outbound destinations.",
+ "provider_scope": "*.turso.io"
+ }
+ ],
+ "actions": [
+ {
+ "name": "load",
+ "summary": "Load the live policy for a perch task. Returns instructions + preference memories + last run.",
+ "description": "Three reads in sequence (each isolated in a try/except so a single library miss does not abort the whole load): (1) config_get('{task_name}-command') → instructions string or null; (2) recall(tags=[task_name, 'preference'], tag_mode='all', n=n_prefs) → preference memory list; (3) _exec SQL over the memories table for the most recent perch-time row tagged with task_name and not skip. Each subquery's failure leaves the corresponding key at its null default. Read-only.",
+ "docs": {
+ "goal": "Resolve a perch task's live policy from Turso.",
+ "inputs_brief": "task_name (req: e.g. 'zeitgeist'|'fly'|'sleep'|'dispatch'), n_prefs (int, default 5)",
+ "outputs_brief": "{instructions: string|null, preferences: array, last_run: object|null}",
+ "errors_brief": "tracking_unconfigured (no Turso creds; partial-success path leaves all three keys at null defaults)",
+ "example": "load task_name='zeitgeist' n_prefs=5"
+ },
+ "invocation": {
+ "kind": "subcommand",
+ "argv_template": [
+ "load",
+ "--task-name",
+ "${input.task_name}",
+ "--n-prefs",
+ "${input.n_prefs}"
+ ]
+ },
+ "input": {
+ "type": "object",
+ "required": [
+ "task_name"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "task_name": {
+ "type": "string",
+ "minLength": 1
+ },
+ "n_prefs": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 100,
+ "default": 5
+ }
+ }
+ },
+ "output": {
+ "format": "json",
+ "schema": {
+ "type": "object",
+ "required": [
+ "instructions",
+ "preferences",
+ "last_run"
+ ],
+ "properties": {
+ "instructions": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "preferences": {
+ "type": "array"
+ },
+ "last_run": {
+ "type": [
+ "object",
+ "null"
+ ]
+ }
+ }
+ }
+ },
+ "side_effects": "read",
+ "idempotent": true,
+ "scopes_used": [
+ "memory.tracking",
+ "net.outbound"
+ ],
+ "error_envelope": "standard",
+ "runtime_telemetry": {}
+ },
+ {
+ "name": "days_since_last_run",
+ "summary": "Return days elapsed since policy.last_run, or null if no prior run is on record. Pure compute.",
+ "description": "Parses last_run.valid_from as ISO datetime (UTC normalized via 'Z' → '+00:00') and returns the float-day delta from now. Returns null when last_run is null or its valid_from is unparseable.",
+ "docs": {
+ "goal": "Compute the age of the most recent prior run.",
+ "inputs_brief": "policy (req: dict from load())",
+ "outputs_brief": "{days: number|null}",
+ "errors_brief": "(none — pure compute, returns null on parse miss)",
+ "example": "days_since_last_run policy={...}"
+ },
+ "invocation": {
+ "kind": "stdin-json",
+ "argv_template": [
+ "days-since-last-run"
+ ]
+ },
+ "input": {
+ "type": "object",
+ "required": [
+ "policy"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "policy": {
+ "type": "object"
+ }
+ }
+ },
+ "output": {
+ "format": "json",
+ "schema": {
+ "type": "object",
+ "required": [
+ "days"
+ ],
+ "properties": {
+ "days": {
+ "type": [
+ "number",
+ "null"
+ ]
+ }
+ }
+ }
+ },
+ "side_effects": "none",
+ "idempotent": true,
+ "scopes_used": [],
+ "error_envelope": "standard",
+ "runtime_telemetry": {}
+ },
+ {
+ "name": "format_summary",
+ "summary": "Render a human-readable one-line summary of a loaded policy, for logging. Pure compute.",
+ "description": "Produces 'task-command ops entry loaded (N chars); M recent preference memories; last run K.Kd ago' (or the corresponding null-branch phrases). Used by perch task prompts when emitting a header line about what policy was loaded.",
+ "docs": {
+ "goal": "Render the policy as a one-line log message.",
+ "inputs_brief": "policy (req: dict from load()), task_name (req)",
+ "outputs_brief": "{summary: string}",
+ "errors_brief": "(none — pure compute)",
+ "example": "format_summary policy={...} task_name='zeitgeist'"
+ },
+ "invocation": {
+ "kind": "stdin-json",
+ "argv_template": [
+ "format-summary"
+ ]
+ },
+ "input": {
+ "type": "object",
+ "required": [
+ "policy",
+ "task_name"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "policy": {
+ "type": "object"
+ },
+ "task_name": {
+ "type": "string",
+ "minLength": 1
+ }
+ }
+ },
+ "output": {
+ "format": "json",
+ "schema": {
+ "type": "object",
+ "required": [
+ "summary"
+ ],
+ "properties": {
+ "summary": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "side_effects": "none",
+ "idempotent": true,
+ "scopes_used": [],
+ "error_envelope": "standard",
+ "runtime_telemetry": {}
+ }
+ ],
+ "data_boundary": {
+ "reads": [
+ {
+ "resource": "memory.tracking",
+ "sensitivity": "medium"
+ }
+ ],
+ "transmits": [],
+ "persists": []
+ },
+ "smoke": {
+ "kind": "shell",
+ "command": [
+ "python",
+ "-c",
+ "from muninn_utils.task_policy import load, days_since_last_run, format_summary\nassert callable(load)\nassert callable(days_since_last_run)\nassert callable(format_summary)\nprint('OK: task_policy public surface present')\n"
+ ],
+ "timeout_seconds": 5,
+ "success": {
+ "exit_code": 0,
+ "stdout_regex": "^OK: task_policy public surface present$"
+ }
+ },
+ "kill_switch": {
+ "kind": "manual",
+ "instructions_url": "https://github.com/oaustegard/muninn-utilities/blob/main/manifests/task-policy/REVOKE.md"
+ },
+ "cost": {
+ "install_fee_cents": 0,
+ "monthly_fee_cents": 0,
+ "usage_model": "none"
+ },
+ "support": {
+ "issues_url": "https://github.com/oaustegard/muninn-utilities/issues",
+ "docs_url": "https://github.com/oaustegard/muninn-utilities/blob/main/muninn_utils/task_policy.py"
+ }
+}
diff --git a/mirrors/manifests/muninn-verify-patch.json b/mirrors/manifests/muninn-verify-patch.json
new file mode 100644
index 0000000..b68567a
--- /dev/null
+++ b/mirrors/manifests/muninn-verify-patch.json
@@ -0,0 +1,308 @@
+{
+ "manifest_version": "0.4",
+ "tool": {
+ "id": "muninn-verify-patch",
+ "version": "0.1.0",
+ "name": "Muninn verify_patch",
+ "summary": "Semi-formal patch verification with outcome tracking. Sends a unified diff plus context to a Claude model with a structured premises/trace/regression-check template, stores the verification result in memory, and exposes review/stamp helpers for post-merge calibration.",
+ "description": "Read-and-reason utility, not a write/execute one. The model is invoked via the orchestrating-agents skill's `claude_client.invoke_claude` (uses ANTHROPIC_API_KEY). Verification results are persisted via the remembering library (uses TURSO_TOKEN, TURSO_URL) so the prediction can later be compared to actual merge outcome via `stamp_verification(tracking_id, outcome)`. The outcome ledger feeds calibration analysis (review_verifications) \u2014 over time, this surfaces patterns of overconfident verdicts.",
+ "homepage": "https://github.com/oaustegard/muninn-utilities/blob/main/muninn_utils/verify_patch.py",
+ "author": {
+ "name": "Muninn (raven of memory; agent operating on behalf of Oskar Austegard)",
+ "url": "https://muninn.austegard.com"
+ },
+ "license": "MIT",
+ "tags": [
+ "verification",
+ "patch-review",
+ "calibration",
+ "anthropic",
+ "memory"
+ ]
+ },
+ "runtime": {
+ "kind": "python-module",
+ "install": {
+ "method": "preinstalled",
+ "locator": {
+ "kind": "python-module",
+ "module": "muninn_utils.verify_patch"
+ }
+ },
+ "entrypoint": {
+ "command": [
+ "python",
+ "-m",
+ "muninn_utils.verify_patch"
+ ]
+ }
+ },
+ "env": [
+ {
+ "name": "ANTHROPIC_API_KEY",
+ "prompt": "Anthropic API key. Used to invoke the Claude model that performs the structured review (sonnet-4-6 by default; configurable per call). Read indirectly via the orchestrating-agents skill's claude_client; verify_patch itself does not reference os.environ for this name.",
+ "secret": true,
+ "required": true,
+ "validation_regex": "^sk-ant-[A-Za-z0-9_-]+$",
+ "obtain_url": "https://console.anthropic.com/settings/keys"
+ },
+ {
+ "name": "TURSO_TOKEN",
+ "prompt": "Turso database token. Used to persist verification predictions and outcomes for calibration analysis. Read indirectly via the remembering library (`from scripts import remember, recall, supersede`).",
+ "secret": true,
+ "required": true,
+ "obtain_url": "https://docs.turso.tech/cli/auth/token"
+ },
+ {
+ "name": "TURSO_URL",
+ "prompt": "Turso database URL. Read indirectly via the remembering library.",
+ "secret": false,
+ "required": true,
+ "validation_regex": "^[A-Za-z0-9._-]+\\.turso\\.io$",
+ "obtain_url": "https://docs.turso.tech/sdk/python/quickstart"
+ }
+ ],
+ "scopes": [
+ {
+ "resource": "anthropic.messages",
+ "actions": [
+ "write"
+ ],
+ "rationale": "Sends the patch + context + structured prompt to the Anthropic Messages API and reads the verdict. Per-call cost depends on patch size and selected model.",
+ "provider_scope": "anthropic-api-key (full account)"
+ },
+ {
+ "resource": "turso.libsql",
+ "actions": [
+ "read",
+ "write"
+ ],
+ "rationale": "Persists the verification record (patch hash, verdict, tracking_id) and later stamps it with merge outcome. Reads via review_verifications to surface the recent prediction ledger.",
+ "provider_scope": "turso-token (single database)"
+ },
+ {
+ "resource": "net.outbound",
+ "actions": [
+ "read",
+ "write"
+ ],
+ "rationale": "api.anthropic.com for model invocation; *.turso.io for memory writes.",
+ "provider_scope": "api.anthropic.com, *.turso.io"
+ }
+ ],
+ "actions": [
+ {
+ "name": "verify_patch",
+ "summary": "Run a structured semi-formal review on a unified diff and persist the verdict for later calibration.",
+ "description": "Embeds the patch and optional context into a fixed template (premises / function resolution / execution trace / regression check / edge cases), invokes the configured Claude model, parses out a verdict, and writes a memory record tagged for later outcome stamping. Returns {tracking_id, verdict, model, raw_response}.",
+ "docs": {
+ "goal": "Get a structured review of a patch with persisted outcome tracking.",
+ "inputs_brief": "patch (req: unified diff text), context (optional), description (optional), model (default claude-sonnet-4-6)",
+ "outputs_brief": "{tracking_id, verdict, model, raw_response}",
+ "errors_brief": "auth_invalid, model_unavailable, storage_failed",
+ "example": "verify_patch patch='--- a/x.py\\n+++ b/x.py\\n...' context='function foo signature' description='fix off-by-one'"
+ },
+ "invocation": {
+ "kind": "stdin-json",
+ "argv_template": [
+ "verify-patch"
+ ]
+ },
+ "input": {
+ "type": "object",
+ "required": [
+ "patch"
+ ],
+ "additionalProperties": true,
+ "properties": {
+ "patch": {
+ "type": "string",
+ "minLength": 1
+ },
+ "context": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "model": {
+ "type": "string",
+ "default": "claude-sonnet-4-6"
+ }
+ }
+ },
+ "output": {
+ "format": "json",
+ "schema": {
+ "type": "object",
+ "required": [
+ "tracking_id",
+ "verdict"
+ ],
+ "properties": {
+ "tracking_id": {
+ "type": "string"
+ },
+ "verdict": {
+ "type": "string"
+ },
+ "model": {
+ "type": "string"
+ },
+ "raw_response": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "side_effects": "write",
+ "idempotent": false,
+ "scopes_used": [
+ "anthropic.messages",
+ "turso.libsql",
+ "net.outbound"
+ ],
+ "error_envelope": "standard",
+ "runtime_telemetry": {}
+ },
+ {
+ "name": "stamp_verification",
+ "summary": "Record the actual outcome (merged/rejected/reverted) for a prior verification, enabling calibration analysis.",
+ "description": "Looks up the memory record by tracking_id and supersedes it with an outcome-stamped version.",
+ "docs": {
+ "goal": "Close the loop on a prediction.",
+ "inputs_brief": "tracking_id (req), outcome (req: merged|rejected|reverted), note (optional)",
+ "outputs_brief": "{stamped: bool, new_id: str}",
+ "errors_brief": "tracking_id_not_found, storage_failed",
+ "example": "stamp_verification tracking_id='abc123' outcome='merged'"
+ },
+ "invocation": {
+ "kind": "stdin-json",
+ "argv_template": [
+ "stamp-verification"
+ ]
+ },
+ "input": {
+ "type": "object",
+ "required": [
+ "tracking_id",
+ "outcome"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "tracking_id": {
+ "type": "string"
+ },
+ "outcome": {
+ "type": "string",
+ "enum": [
+ "merged",
+ "rejected",
+ "reverted"
+ ]
+ },
+ "note": {
+ "type": "string"
+ }
+ }
+ },
+ "output": {
+ "format": "json",
+ "schema": {
+ "type": "object",
+ "required": [
+ "stamped"
+ ],
+ "properties": {
+ "stamped": {
+ "type": "boolean"
+ },
+ "new_id": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "side_effects": "write",
+ "idempotent": true,
+ "scopes_used": [
+ "turso.libsql"
+ ],
+ "error_envelope": "standard",
+ "runtime_telemetry": {}
+ }
+ ],
+ "data_boundary": {
+ "reads": [
+ {
+ "resource": "turso.libsql",
+ "sensitivity": "medium"
+ }
+ ],
+ "transmits": [
+ {
+ "to": "api.anthropic.com",
+ "fields": [
+ "env.ANTHROPIC_API_KEY",
+ "input.patch",
+ "input.context",
+ "input.description"
+ ],
+ "purpose": "model invocation for structured review",
+ "third_party_retention": "persistent-30d",
+ "vendor_tos_url": "https://www.anthropic.com/legal/commercial-terms"
+ },
+ {
+ "to": "*.turso.io",
+ "fields": [
+ "env.TURSO_TOKEN",
+ "computed.verdict",
+ "computed.tracking_id"
+ ],
+ "purpose": "persist verification record and outcome stamp",
+ "third_party_retention": "persistent-indefinite",
+ "vendor_tos_url": "https://turso.tech/legal/terms-of-service"
+ }
+ ],
+ "persists": [
+ {
+ "where": "tool_local",
+ "fields": [
+ "patch_hash",
+ "verdict",
+ "tracking_id",
+ "outcome"
+ ]
+ }
+ ],
+ "retention": {
+ "tool_local_days": 365
+ }
+ },
+ "smoke": {
+ "kind": "shell",
+ "command": [
+ "python",
+ "-c",
+ "import os\nassert os.environ.get('ANTHROPIC_API_KEY'), 'ANTHROPIC_API_KEY not set'\nassert os.environ.get('TURSO_TOKEN'), 'TURSO_TOKEN not set'\nassert os.environ.get('TURSO_URL'), 'TURSO_URL not set'\nprint('OK: required env present')\n"
+ ],
+ "timeout_seconds": 5,
+ "success": {
+ "exit_code": 0,
+ "stdout_regex": "OK: required env present"
+ }
+ },
+ "kill_switch": {
+ "kind": "manual",
+ "instructions_url": "https://github.com/oaustegard/muninn-utilities/blob/main/manifests/verify-patch/REVOKE.md"
+ },
+ "cost": {
+ "install_fee_cents": 0,
+ "monthly_fee_cents": 0,
+ "usage_model": "per-call"
+ },
+ "support": {
+ "issues_url": "https://github.com/oaustegard/muninn-utilities/issues",
+ "docs_url": "https://github.com/oaustegard/muninn-utilities/blob/main/muninn_utils/verify_patch.py"
+ }
+}
diff --git a/mirrors/manifests/muninn-whtwnd.json b/mirrors/manifests/muninn-whtwnd.json
new file mode 100644
index 0000000..87bfb24
--- /dev/null
+++ b/mirrors/manifests/muninn-whtwnd.json
@@ -0,0 +1,479 @@
+{
+ "manifest_version": "0.4",
+ "tool": {
+ "id": "muninn-whtwnd",
+ "version": "0.1.0",
+ "name": "Muninn whtwnd",
+ "summary": "Publish, update, delete, and list WhiteWind blog entries via ATProto. Posts land in the user's PDS as `com.whtwnd.blog.entry` records and federate to the WhiteWind AppView.",
+ "description": "Five operations: post, update, delete, list, upload-image. Auth via the same `com.atproto.server.createSession` flow as bsky_card \u2014 the same handle and app password reach two distinct AppViews (Bluesky's and WhiteWind's) because both ride on the same PDS. Image attachments are uploaded as ATProto blobs and embedded in the entry's `blobs[]`; the URL pattern is the PDS-served `getBlob` endpoint, NOT the bsky CDN (which 500s for non-Bluesky records). The tool's only persistence is the records it creates on the user's PDS \u2014 those are the user's own data on the user's own infrastructure, not third-party transmissions. Issue #5 calls this out as the third atproto-credential utility (after bsky_card and the bsky-announce chain in blog_publish), the third publishing target (after perch_publish and blog_publish), and a novel-domain test (a federated atproto blog rather than a Bluesky post).",
+ "homepage": "https://github.com/oaustegard/muninn-utilities/blob/main/muninn_utils/whtwnd.py",
+ "author": {
+ "name": "Muninn (raven of memory; agent operating on behalf of Oskar Austegard)",
+ "url": "https://muninn.austegard.com"
+ },
+ "license": "MIT",
+ "tags": [
+ "atproto",
+ "whtwnd",
+ "blog",
+ "publishing",
+ "federated"
+ ]
+ },
+ "runtime": {
+ "kind": "python-module",
+ "install": {
+ "method": "preinstalled",
+ "locator": {
+ "kind": "python-module",
+ "module": "muninn_utils.whtwnd"
+ }
+ },
+ "entrypoint": {
+ "command": [
+ "python",
+ "-m",
+ "muninn_utils.whtwnd"
+ ]
+ }
+ },
+ "env": [
+ {
+ "name": "BSKY_HANDLE",
+ "prompt": "The atproto handle whose PDS hosts the WhiteWind entries, e.g. 'austegard.com'. Same credential pair as bsky_card \u2014 handles do not differ between AppViews on the same PDS.",
+ "secret": false,
+ "required": true,
+ "validation_regex": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$",
+ "obtain_url": "https://bsky.app/settings/account"
+ },
+ {
+ "name": "BSKY_APP_PASSWORD",
+ "prompt": "App password for the handle. Treat as a secret. Cannot be programmatically revoked \u2014 revoke at https://bsky.app/settings/app-passwords.",
+ "secret": true,
+ "required": true,
+ "validation_regex": "^[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}$",
+ "obtain_url": "https://bsky.app/settings/app-passwords"
+ }
+ ],
+ "scopes": [
+ {
+ "resource": "atproto.repo",
+ "actions": [
+ "read",
+ "write",
+ "delete"
+ ],
+ "rationale": "Creates, updates, deletes `com.whtwnd.blog.entry` records on the user's PDS. Uploads image blobs and embeds them in entries.",
+ "provider_scope": "app-password (coarse; full repo write except DMs and account deletion)"
+ },
+ {
+ "resource": "net.outbound",
+ "actions": [
+ "read",
+ "write"
+ ],
+ "rationale": "Talks to the user's PDS (default bsky.social) for atproto operations. No other outbound destinations \u2014 WhiteWind's AppView reads federated content directly from the PDS without this tool contacting whtwnd.com.",
+ "provider_scope": "*.bsky.social, *.bsky.network"
+ }
+ ],
+ "actions": [
+ {
+ "name": "post",
+ "summary": "Create a new WhiteWind blog entry on the user's PDS.",
+ "description": "Calls com.atproto.repo.createRecord with collection=com.whtwnd.blog.entry. The entry's `content` field is markdown; `title` is the display title. Optional `blobs` parameter embeds previously-uploaded image blobs so the PDS does not garbage-collect them. Destructive: a created entry federates immediately and is publicly visible at whtwnd.com//.",
+ "docs": {
+ "goal": "Publish a new WhiteWind blog post.",
+ "inputs_brief": "content (req, markdown), title (req), blobs (optional blob_metadata array)",
+ "outputs_brief": "{post_url, uri, cid, rkey}",
+ "errors_brief": "auth_invalid, content_too_long, blob_not_found, network_unreachable",
+ "example": "post content='# Title\\n\\nBody...' title='My Post'"
+ },
+ "invocation": {
+ "kind": "stdin-json",
+ "argv_template": [
+ "post"
+ ]
+ },
+ "input": {
+ "type": "object",
+ "required": [
+ "content",
+ "title"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "content": {
+ "type": "string",
+ "minLength": 1
+ },
+ "title": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 300
+ },
+ "blobs": {
+ "type": "array"
+ }
+ }
+ },
+ "output": {
+ "format": "json",
+ "schema": {
+ "type": "object",
+ "required": [
+ "post_url",
+ "uri",
+ "cid"
+ ],
+ "properties": {
+ "post_url": {
+ "type": "string",
+ "format": "uri"
+ },
+ "uri": {
+ "type": "string",
+ "pattern": "^at://"
+ },
+ "cid": {
+ "type": "string"
+ },
+ "rkey": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "side_effects": "destructive",
+ "idempotent": false,
+ "scopes_used": [
+ "atproto.repo",
+ "net.outbound"
+ ],
+ "error_envelope": "standard",
+ "runtime_telemetry": {}
+ },
+ {
+ "name": "update",
+ "summary": "Update an existing WhiteWind entry by rkey.",
+ "description": "Calls com.atproto.repo.putRecord against the same collection + rkey. Replaces title and content. The entry's CID changes; previous CIDs are no longer canonical.",
+ "docs": {
+ "goal": "Edit a previously-published WhiteWind entry.",
+ "inputs_brief": "rkey (req), content (req), title (req), blobs (optional)",
+ "outputs_brief": "{post_url, uri, cid, rkey}",
+ "errors_brief": "auth_invalid, rkey_not_found, network_unreachable",
+ "example": "update rkey='3l5...' content='...' title='Edited'"
+ },
+ "invocation": {
+ "kind": "stdin-json",
+ "argv_template": [
+ "update"
+ ]
+ },
+ "input": {
+ "type": "object",
+ "required": [
+ "rkey",
+ "content",
+ "title"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "rkey": {
+ "type": "string",
+ "minLength": 1
+ },
+ "content": {
+ "type": "string",
+ "minLength": 1
+ },
+ "title": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 300
+ },
+ "blobs": {
+ "type": "array"
+ }
+ }
+ },
+ "output": {
+ "format": "json",
+ "schema": {
+ "type": "object",
+ "required": [
+ "post_url",
+ "uri",
+ "cid"
+ ],
+ "properties": {
+ "post_url": {
+ "type": "string",
+ "format": "uri"
+ },
+ "uri": {
+ "type": "string",
+ "pattern": "^at://"
+ },
+ "cid": {
+ "type": "string"
+ },
+ "rkey": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "side_effects": "write",
+ "idempotent": true,
+ "scopes_used": [
+ "atproto.repo",
+ "net.outbound"
+ ],
+ "error_envelope": "standard",
+ "runtime_telemetry": {}
+ },
+ {
+ "name": "delete",
+ "summary": "Delete a WhiteWind entry by rkey.",
+ "description": "Calls com.atproto.repo.deleteRecord. Idempotent at the AppView (deleting a non-existent record is a no-op).",
+ "docs": {
+ "goal": "Retract a WhiteWind entry.",
+ "inputs_brief": "rkey (req)",
+ "outputs_brief": "{deleted: bool}",
+ "errors_brief": "auth_invalid, network_unreachable",
+ "example": "delete rkey='3l5...'"
+ },
+ "invocation": {
+ "kind": "stdin-json",
+ "argv_template": [
+ "delete"
+ ]
+ },
+ "input": {
+ "type": "object",
+ "required": [
+ "rkey"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "rkey": {
+ "type": "string",
+ "minLength": 1
+ }
+ }
+ },
+ "output": {
+ "format": "json",
+ "schema": {
+ "type": "object",
+ "required": [
+ "deleted"
+ ],
+ "properties": {
+ "deleted": {
+ "type": "boolean"
+ }
+ }
+ }
+ },
+ "side_effects": "destructive",
+ "idempotent": true,
+ "scopes_used": [
+ "atproto.repo",
+ "net.outbound"
+ ],
+ "error_envelope": "standard",
+ "runtime_telemetry": {}
+ },
+ {
+ "name": "list",
+ "summary": "List the user's WhiteWind entries (read-only).",
+ "description": "Calls com.atproto.repo.listRecords with collection=com.whtwnd.blog.entry. Returns rkey, title, createdAt, and a content preview for each entry.",
+ "docs": {
+ "goal": "Enumerate the user's published WhiteWind entries.",
+ "inputs_brief": "limit (int, default 50, max 100)",
+ "outputs_brief": "{entries: [{rkey, title, createdAt, preview}]}",
+ "errors_brief": "auth_invalid, network_unreachable",
+ "example": "list limit=20"
+ },
+ "invocation": {
+ "kind": "subcommand",
+ "argv_template": [
+ "list",
+ "--limit",
+ "${input.limit}"
+ ]
+ },
+ "input": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "limit": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 100,
+ "default": 50
+ }
+ }
+ },
+ "output": {
+ "format": "json",
+ "schema": {
+ "type": "object",
+ "required": [
+ "entries"
+ ],
+ "properties": {
+ "entries": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "rkey": {
+ "type": "string"
+ },
+ "title": {
+ "type": "string"
+ },
+ "createdAt": {
+ "type": "string"
+ },
+ "preview": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "side_effects": "read",
+ "idempotent": true,
+ "scopes_used": [
+ "atproto.repo",
+ "net.outbound"
+ ],
+ "error_envelope": "standard",
+ "runtime_telemetry": {}
+ },
+ {
+ "name": "upload_image",
+ "summary": "Upload a local image as an ATProto blob and return blob_metadata for embedding in a post/update.",
+ "description": "Calls com.atproto.repo.uploadBlob. Returns blob_metadata that MUST be included in a subsequent post/update's `blobs[]` parameter or the PDS will garbage-collect the blob. The `markdown` field in the returned metadata is a ready-to-paste `` snippet using the PDS's getBlob endpoint.",
+ "docs": {
+ "goal": "Upload an image so it can be embedded in a WhiteWind entry.",
+ "inputs_brief": "image_path (req, local file path)",
+ "outputs_brief": "{blob_metadata, markdown, url, cid}",
+ "errors_brief": "file_not_found, mime_unsupported, blob_too_large, network_unreachable",
+ "example": "upload_image image_path='/tmp/header.png'"
+ },
+ "invocation": {
+ "kind": "stdin-json",
+ "argv_template": [
+ "upload-image"
+ ]
+ },
+ "input": {
+ "type": "object",
+ "required": [
+ "image_path"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "image_path": {
+ "type": "string",
+ "minLength": 1
+ }
+ }
+ },
+ "output": {
+ "format": "json",
+ "schema": {
+ "type": "object",
+ "required": [
+ "blob_metadata",
+ "url"
+ ],
+ "properties": {
+ "blob_metadata": {
+ "type": "object"
+ },
+ "markdown": {
+ "type": "string"
+ },
+ "url": {
+ "type": "string",
+ "format": "uri"
+ },
+ "cid": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "side_effects": "write",
+ "idempotent": false,
+ "scopes_used": [
+ "atproto.repo",
+ "net.outbound"
+ ],
+ "error_envelope": "standard",
+ "runtime_telemetry": {}
+ }
+ ],
+ "data_boundary": {
+ "reads": [
+ {
+ "resource": "atproto.repo.posts",
+ "sensitivity": "low"
+ },
+ {
+ "resource": "fs.local",
+ "sensitivity": "low"
+ }
+ ],
+ "transmits": [
+ {
+ "to": "bsky.social",
+ "fields": [
+ "input.title",
+ "input.content",
+ "input.blobs",
+ "uploaded_image_bytes"
+ ],
+ "purpose": "publish whtwnd entry to user's PDS; PDS federates to whtwnd.com AppView. Entries are public by design.",
+ "third_party_retention": "persistent-indefinite"
+ }
+ ],
+ "persists": [],
+ "retention": {
+ "tool_local_days": 0
+ }
+ },
+ "smoke": {
+ "kind": "shell",
+ "command": [
+ "python",
+ "-c",
+ "from muninn_utils.whtwnd import whtwnd_auth\nimport os\nauth = whtwnd_auth(os.environ['BSKY_HANDLE'], os.environ['BSKY_APP_PASSWORD'])\nassert auth['did'].startswith('did:'), 'auth resolved no DID'\nprint('OK: auth resolved to', auth['did'])\n"
+ ],
+ "timeout_seconds": 15,
+ "success": {
+ "exit_code": 0,
+ "stdout_regex": "^OK: auth resolved to did:"
+ }
+ },
+ "kill_switch": {
+ "kind": "url",
+ "url": "https://bsky.app/settings/app-passwords"
+ },
+ "cost": {
+ "install_fee_cents": 0,
+ "monthly_fee_cents": 0,
+ "usage_model": "none"
+ },
+ "support": {
+ "issues_url": "https://github.com/oaustegard/muninn-utilities/issues",
+ "docs_url": "https://github.com/oaustegard/muninn-utilities/blob/main/muninn_utils/whtwnd.py"
+ }
+}
diff --git a/mirrors/manifests/muninn-zeitgeist-delta.json b/mirrors/manifests/muninn-zeitgeist-delta.json
new file mode 100644
index 0000000..19aff8f
--- /dev/null
+++ b/mirrors/manifests/muninn-zeitgeist-delta.json
@@ -0,0 +1,261 @@
+{
+ "manifest_version": "0.4",
+ "tool": {
+ "id": "muninn-zeitgeist-delta",
+ "version": "0.1.0",
+ "name": "Muninn zeitgeist_delta",
+ "summary": "Semantic deduplication for zeitgeist drafts. Compares each topic section of a candidate zeitgeist memory against the N most-recent stored zeitgeists using Gemini embeddings (via Cloudflare AI Gateway) and emits a delta-only compressed version.",
+ "description": "Reads recent `zeitgeist`-tagged memories from Turso, splits the candidate draft into topic sections by markdown header, embeds each section + each prior memory's sections via Gemini's text-embedding model (proxied through Cloudflare AI Gateway), and computes cosine similarity. Sections above a duplicate threshold are flagged as redundant; the rest pass through into a compressed delta-only version that can be stored without bloating the memory store. Like `verify_patch` (LLM-as-judge with Anthropic + optional Turso writes) but with embeddings instead of completions and a different cloud surface (Cloudflare AI Gateway \u2192 Gemini, not Anthropic). Issue #5 calls this out as the test for whether two LLM-as-judge tools share a manifest pattern.",
+ "homepage": "https://github.com/oaustegard/muninn-utilities/blob/main/muninn_utils/zeitgeist_delta.py",
+ "author": {
+ "name": "Muninn (raven of memory; agent operating on behalf of Oskar Austegard)",
+ "url": "https://muninn.austegard.com"
+ },
+ "license": "MIT",
+ "tags": [
+ "embeddings",
+ "deduplication",
+ "memory",
+ "llm",
+ "gemini",
+ "cloudflare-ai-gateway"
+ ]
+ },
+ "runtime": {
+ "kind": "python-module",
+ "install": {
+ "method": "preinstalled",
+ "locator": {
+ "kind": "python-module",
+ "module": "muninn_utils.zeitgeist_delta"
+ }
+ },
+ "entrypoint": {
+ "command": [
+ "python",
+ "-m",
+ "muninn_utils.zeitgeist_delta"
+ ]
+ }
+ },
+ "env": [
+ {
+ "name": "CF_ACCOUNT_ID",
+ "prompt": "Cloudflare account ID hosting the AI Gateway used to proxy Gemini embedding calls. The tool routes through Cloudflare for caching, observability, and rate-limit pooling.",
+ "secret": false,
+ "required": true,
+ "validation_regex": "^[a-f0-9]{32}$"
+ },
+ {
+ "name": "CF_GATEWAY_ID",
+ "prompt": "Cloudflare AI Gateway slug \u2014 the gateway name chosen when it was created in the Cloudflare dashboard.",
+ "secret": false,
+ "required": true
+ },
+ {
+ "name": "CF_API_TOKEN",
+ "prompt": "Cloudflare API token scoped to AI Gateway access. Treat as a secret. Required.",
+ "secret": true,
+ "required": true
+ },
+ {
+ "name": "TURSO_TOKEN",
+ "prompt": "Turso libSQL auth token for the Muninn memory database. The tool reads recent zeitgeist memories to compare against. Required.",
+ "secret": true,
+ "required": true,
+ "obtain_url": "https://app.turso.tech/"
+ },
+ {
+ "name": "TURSO_URL",
+ "prompt": "Hostname of the Muninn memory libSQL database, e.g. 'mydb-username.turso.io'.",
+ "secret": false,
+ "required": true,
+ "validation_regex": "^[a-z0-9-]+\\.[a-z0-9-]+\\.turso\\.io$"
+ }
+ ],
+ "scopes": [
+ {
+ "resource": "compute.llm-inference",
+ "actions": [
+ "read",
+ "write"
+ ],
+ "rationale": "Each check_delta call sends the candidate draft and N prior memory sections to Gemini's text-embedding model, consuming tokens against the user's Cloudflare AI Gateway quota. Cost is variable in candidate length \u00d7 N.",
+ "provider_scope": "cf-api-token (coarse; full AI Gateway access)"
+ },
+ {
+ "resource": "memory.tracking",
+ "actions": [
+ "read"
+ ],
+ "rationale": "Reads the N most-recent `zeitgeist`-tagged memories from Turso to compare against. Read-only; the tool returns a delta but does not write it back \u2014 the caller decides whether to store.",
+ "provider_scope": "turso-libsql-token (coarse; full DB access)"
+ },
+ {
+ "resource": "net.outbound",
+ "actions": [
+ "read",
+ "write"
+ ],
+ "rationale": "Talks to gateway.ai.cloudflare.com (which forwards to Gemini's embedding endpoint) and to the configured Turso libSQL host for memory reads.",
+ "provider_scope": "gateway.ai.cloudflare.com, *.turso.io"
+ }
+ ],
+ "actions": [
+ {
+ "name": "check_delta",
+ "summary": "Compare a candidate zeitgeist draft against recent stored zeitgeists and return per-section duplicate flags plus a compressed delta-only version.",
+ "description": "Synchronously: fetches recent memories, splits candidate into sections by markdown headers, embeds all sections, computes pairwise cosine similarity, flags sections above the threshold as redundant. Returns the report (per-section verdicts) and a compressed `delta_text` containing only the non-redundant sections. Read-only \u2014 does not write the delta back to Turso. Cost is variable in `n_recent` \u00d7 candidate length; budget ~$0.0001 per call at typical sizes.",
+ "docs": {
+ "goal": "Identify which sections of a candidate zeitgeist memory duplicate prior entries, and emit a compressed delta.",
+ "inputs_brief": "draft (req markdown), n_recent (int, default 5), threshold (float, default 0.85)",
+ "outputs_brief": "{report: [{section, verdict, similarity, ref_id?}], delta_text: string, total_sections: int, redundant_count: int}",
+ "errors_brief": "tracking_unconfigured, gateway_unreachable, embed_failed, draft_empty",
+ "example": "check_delta draft='# Topic A\\n...' n_recent=5 threshold=0.85"
+ },
+ "invocation": {
+ "kind": "stdin-json",
+ "argv_template": [
+ "check-delta"
+ ]
+ },
+ "input": {
+ "type": "object",
+ "required": [
+ "draft"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "draft": {
+ "type": "string",
+ "minLength": 1
+ },
+ "n_recent": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 50,
+ "default": 5
+ },
+ "threshold": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 1,
+ "default": 0.85
+ }
+ }
+ },
+ "output": {
+ "format": "json",
+ "schema": {
+ "type": "object",
+ "required": [
+ "report",
+ "delta_text",
+ "total_sections",
+ "redundant_count"
+ ],
+ "properties": {
+ "report": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "section": {
+ "type": "string"
+ },
+ "verdict": {
+ "type": "string",
+ "enum": [
+ "NOVEL",
+ "REDUNDANT"
+ ]
+ },
+ "similarity": {
+ "type": "number"
+ },
+ "ref_id": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ }
+ }
+ },
+ "delta_text": {
+ "type": "string"
+ },
+ "total_sections": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "redundant_count": {
+ "type": "integer",
+ "minimum": 0
+ }
+ }
+ }
+ },
+ "side_effects": "read",
+ "idempotent": true,
+ "scopes_used": [
+ "compute.llm-inference",
+ "memory.tracking",
+ "net.outbound"
+ ],
+ "error_envelope": "standard",
+ "runtime_telemetry": {}
+ }
+ ],
+ "data_boundary": {
+ "reads": [
+ {
+ "resource": "compute.llm-inference",
+ "sensitivity": "low"
+ },
+ {
+ "resource": "memory.tracking",
+ "sensitivity": "medium"
+ }
+ ],
+ "transmits": [
+ {
+ "to": "gateway.ai.cloudflare.com",
+ "fields": [
+ "input.draft",
+ "fetched_zeitgeist_section_text"
+ ],
+ "purpose": "model-inference (embeddings; proxied through Cloudflare AI Gateway to Gemini)",
+ "third_party_retention": "unknown",
+ "vendor_tos_url": "https://www.cloudflare.com/service-specific-terms-application-services/#ai-gateway"
+ }
+ ],
+ "persists": []
+ },
+ "smoke": {
+ "kind": "shell",
+ "command": [
+ "python",
+ "-c",
+ "from muninn_utils.zeitgeist_delta import _ensure_proxy_env\n_ensure_proxy_env()\nimport os\nfor k in ('CF_ACCOUNT_ID', 'CF_GATEWAY_ID', 'CF_API_TOKEN'):\n assert os.environ.get(k), f'{k} not set'\nprint('OK: proxy env loaded')\n"
+ ],
+ "timeout_seconds": 5,
+ "success": {
+ "exit_code": 0,
+ "stdout_regex": "^OK: proxy env loaded$"
+ }
+ },
+ "kill_switch": {
+ "kind": "manual",
+ "instructions_url": "https://github.com/oaustegard/muninn-utilities/blob/main/manifests/zeitgeist-delta/REVOKE.md"
+ },
+ "cost": {
+ "install_fee_cents": 0,
+ "monthly_fee_cents": 0,
+ "usage_model": "external"
+ },
+ "support": {
+ "issues_url": "https://github.com/oaustegard/muninn-utilities/issues",
+ "docs_url": "https://github.com/oaustegard/muninn-utilities/blob/main/muninn_utils/zeitgeist_delta.py"
+ }
+}
diff --git a/mirrors/manifests/yep-memory.json b/mirrors/manifests/yep-memory.json
new file mode 100644
index 0000000..ef55c41
--- /dev/null
+++ b/mirrors/manifests/yep-memory.json
@@ -0,0 +1,140 @@
+{
+ "manifest_version": "0.4",
+ "tool": {
+ "id": "yep-memory",
+ "namespace": "yep",
+ "version": "0.1.0",
+ "name": "Yep Memory",
+ "summary": "Persistent memory primitives for personal agents \u2014 facts, reflections, and episodes with semantic recall across conversations.",
+ "description": "Yep's memory subsystem exposes three orthogonal stores: facts (key-value, upsert), reflections (free-form learned generalizations), and episodes (event log with semantic search). Used internally by Yep and surfaced here as the canonical example for any personal-agent stack that wants long-running cross-session memory. Backed by Supabase + pgvector; reflection writes are deliberate and sparse, not autopilot. Published as the first dogfood entry on drknowhow/Yep to validate the federation pipeline end-to-end against toolspace.yepgent.com.",
+ "homepage": "https://yepgent.com",
+ "author": {
+ "name": "Yep (agent on behalf of Dimitri T)",
+ "url": "https://yepgent.com"
+ },
+ "license": "MIT",
+ "tags": ["memory", "personal-agent", "semantic-search", "supabase", "pgvector"]
+ },
+ "runtime": {
+ "kind": "python-module",
+ "install": {
+ "method": "preinstalled",
+ "locator": {
+ "kind": "python-module",
+ "module": "src.memory"
+ }
+ },
+ "entrypoint": {
+ "command": ["python", "-m", "src.memory"]
+ }
+ },
+ "scopes": [
+ {
+ "resource": "db.memory",
+ "actions": ["read", "write"],
+ "rationale": "Reads and writes facts, reflections, and episodes to the agent's persistent memory store."
+ }
+ ],
+ "data_boundary": {
+ "reads": [
+ {
+ "resource": "db.memory",
+ "sensitivity": "medium"
+ }
+ ],
+ "transmits": [
+ {
+ "to_kind": "agent-supplied",
+ "to_constraint": "Supabase instance configured for this agent only.",
+ "fields": ["facts.*", "reflections.*", "episodes.*"],
+ "purpose": "persistence",
+ "third_party_retention": "persistent-indefinite"
+ }
+ ]
+ },
+ "actions": [
+ {
+ "name": "remember_fact",
+ "summary": "Store or update a key-value fact. Upserts on key.",
+ "invocation": {
+ "kind": "subcommand",
+ "argv_template": ["remember_fact", "--key", "${input.key}", "--value", "${input.value}"]
+ },
+ "input": {
+ "type": "object",
+ "required": ["key", "value"],
+ "additionalProperties": false,
+ "properties": {
+ "key": {"type": "string", "minLength": 1, "description": "Dotted key, e.g. 'user.timezone'."},
+ "value": {"type": "string"}
+ }
+ },
+ "output": {"format": "json"},
+ "side_effects": "write",
+ "idempotent": true,
+ "scopes_used": ["db.memory"]
+ },
+ {
+ "name": "recall_fact",
+ "summary": "Retrieve a fact by exact key.",
+ "invocation": {
+ "kind": "subcommand",
+ "argv_template": ["recall_fact", "--key", "${input.key}"]
+ },
+ "input": {
+ "type": "object",
+ "required": ["key"],
+ "additionalProperties": false,
+ "properties": {
+ "key": {"type": "string", "minLength": 1}
+ }
+ },
+ "output": {"format": "json"},
+ "side_effects": "read",
+ "idempotent": true,
+ "scopes_used": ["db.memory"]
+ },
+ {
+ "name": "reflect",
+ "summary": "Write a reflection (learned generalization). Use sparingly \u2014 reflections are precious.",
+ "invocation": {
+ "kind": "subcommand",
+ "argv_template": ["reflect", "--content", "${input.content}"]
+ },
+ "input": {
+ "type": "object",
+ "required": ["content"],
+ "additionalProperties": false,
+ "properties": {
+ "content": {"type": "string", "minLength": 1, "maxLength": 4000}
+ }
+ },
+ "output": {"format": "json"},
+ "side_effects": "write",
+ "idempotent": false,
+ "scopes_used": ["db.memory"]
+ }
+ ],
+ "smoke": {
+ "kind": "action-call",
+ "action": "recall_fact",
+ "arguments": {"key": "registry.smoke_test"},
+ "timeout_seconds": 5,
+ "success": {
+ "exit_code": 0
+ }
+ },
+ "kill_switch": {
+ "kind": "manual",
+ "instructions": "Revoke by either (a) unsetting SUPABASE_URL / SUPABASE_KEY in the runtime env \u2014 the memory module fails closed without DB credentials, or (b) removing src.memory from the registered tool surface in src/core/mcp_registry.py. The module is read-write; revoking DB access stops both ingestion and recall in the same step."
+ },
+ "cost": {
+ "install_fee_cents": 0,
+ "monthly_fee_cents": 0,
+ "usage_model": "external"
+ },
+ "support": {
+ "issues_url": "https://yepgent.com",
+ "docs_url": "https://toolspace.yepgent.com/registry/yep-memory/"
+ }
+}
diff --git a/registry/deep-research/index.html b/registry/deep-research/index.html
index 8c29576..8c6d1ef 100644
--- a/registry/deep-research/index.html
+++ b/registry/deep-research/index.html
@@ -642,7 +642,7 @@
deep_research is an agent-runtime-agnostic workflow that turns an empirical research question into a citation-grade synthesis. Two human-in-the-loop gates (protocol pre-registration, Pass-2 spend) bracket a four-role subagent crew (Scout / Skeptic / Methodologist / Synthesizer). Every claim a synthesis ships must be backed by a row in the research_evidence table with a verbatim quote_span; claims without their quote get cut. v0.2.0 adds the Python reference implementation: a stdlib-urllib scholar adapter (OpenAlex / Semantic Scholar / PubMed / arXiv / Europe PMC / Crossref / Unpaywall) and a python-docx + matplotlib synthesis builder with pluggable upload. Core stays stdlib-only; the docx builder is gated behind the optional `[viz]` extra.
Tags: researchliterature-reviewmeta-analysisagentssubagent-fan-outno-fabricationskill-bundle
License: Apache-2.0
- Last fetched 2026-06-18T09:09:15Z (live)
+ Last fetched 2026-07-09T17:47:58Z (mirror)
- Pages generated 2026-06-18T09:09:15Z · manifests.json · changelog.json
+ Pages generated 2026-07-09T17:47:58Z · manifests.json · changelog.json