diff --git a/skills/inbox-triage/SKILL.md b/skills/inbox-triage/SKILL.md new file mode 100644 index 00000000..2f603f7f --- /dev/null +++ b/skills/inbox-triage/SKILL.md @@ -0,0 +1,131 @@ +--- +name: inbox-triage +version: 0.1.0 +description: Classify a bounded inbox thread, route it to the right queue, draft a safe reply when possible, and stop before any send action. +source: + type: cli-tool + command: node + args: + - run.mjs +links: + source: https://github.com/LubuSeb/runx/tree/lubu/inbox-triage-34/skills/inbox-triage +runx: + category: ops + input_resolution: + required: + - inbox_packet + - operator_policy +--- + +# Inbox Triage + +Classify one bounded inbox thread, decide the safest queue, draft a reply only +when the supplied context is enough, and return a send proposal that requires a +separate approval gate. The skill reads fixture-style inbox packets and never +connects to a mailbox, sends mail, mutates tickets, or accesses private account +state. + +## When To Use + +Use this skill when an operator or agent has an already-bounded inbox packet and +needs a safe first-pass decision: + +- classify the message intent and urgency; +- route the thread to a named queue; +- prepare a reply draft for low-risk informational messages; +- preserve the evidence and citations used for that decision; +- hand off any send through `send-as` or another governed sender. + +## When Not To Use + +Do not use this skill as a mailbox connector, live sending tool, account +recovery authority, billing operator, abuse moderator, or customer identity +verifier. Do not pass raw mailbox exports, unrelated threads, credentials, +private account records, or broad contact lists. If the request needs private +state, identity proof, payment action, legal review, abuse handling, or an +unapproved send, the skill must return a blocked/manual-review proposal. + +## Procedure + +1. Require `inbox_packet` to contain a source, thread id, and at least one + message with sender metadata and body text. Stop with a failure receipt when + those bounded-context fields are missing. +2. Normalize the latest message and classify it as `product_question`, + `bug_report`, `billing`, `account_access`, `abuse`, `unsafe_send_request`, or + `unknown`. +3. Choose a queue from `operator_policy.queues` or a safe default. +4. Draft a reply only for low-risk product questions where sender metadata and + body text are present. +5. For bug, billing, account, abuse, unknown, or unsafe-send cases, produce no + reply body and route to review. +6. Always emit `gated_send_proposal.decision = "requires_human_approval"` or a + stricter blocked state; never authorize delivery. +7. Include cited message ids, matched signals, missing context, and the exact + send-as handoff requirements. + +## Output Shape + +```json +{ + "classification": { + "label": "product_question", + "confidence": 0.88, + "urgency": "normal", + "matched_signals": ["how do i", "setup"], + "rationale": "The message asks a bounded setup question." + }, + "triage_queue": { + "name": "support.reply_drafts", + "priority": "normal", + "reason": "Safe product question with enough context for a draft.", + "cited_message_ids": ["msg-1"], + "missing_context": [] + }, + "draft_reply": { + "proposed": true, + "to": "mira@example.test", + "subject": "Re: Verify sending domain", + "body": "..." + }, + "gated_send_proposal": { + "decision": "requires_human_approval", + "send_as_skill": "send-as", + "approval_required": true, + "blocked_reason": null + } +} +``` + +## Send-As Composition + +This skill only prepares a reply draft. A downstream sender must bind the +principal, recipient, content digest, provider account, consent basis, and human +approval before delivery. The output names the send-as handoff but does not +perform it. + +## Worked Example + +```bash +runx skill "$PWD" \ + --runner triage \ + --input-json inbox_packet='{ + "thread_id": "thr-100", + "source": "fixture:safe-product-question", + "messages": [{ + "id": "msg-1", + "from": {"name": "Mira", "email": "mira@example.test"}, + "subject": "Verify sending domain", + "body": "How do I finish the DNS verification step?" + }] + }' \ + --input-json operator_policy='{ + "product_name": "ExampleDesk", + "support_signature": "ExampleDesk Support", + "queues": {"reply_drafts": "support.reply_drafts"} + }' \ + --json +``` + +Expected result: `classification.label = product_question`, +`triage_queue.name = support.reply_drafts`, `draft_reply.proposed = true`, and +`gated_send_proposal.decision = requires_human_approval`. diff --git a/skills/inbox-triage/X.yaml b/skills/inbox-triage/X.yaml new file mode 100644 index 00000000..da0f61a7 --- /dev/null +++ b/skills/inbox-triage/X.yaml @@ -0,0 +1,118 @@ +skill: inbox-triage +version: "0.1.0" + +catalog: + kind: skill + audience: public + visibility: public + role: canonical + +harness: + cases: + - name: safe-product-question + runner: triage + inputs: + inbox_packet: + thread_id: thr-100 + source: fixture:safe-product-question + received_at: "2026-06-22T12:00:00Z" + messages: + - id: msg-1 + from: + name: Mira Holm + email: mira@example.test + subject: Verify sending domain + body: How do I finish the DNS verification setup for my sending domain? I added the SPF and DKIM records. + operator_policy: + product_name: ExampleDesk + support_signature: ExampleDesk Support + principal_ref: team:exampledesk-support + queues: + reply_drafts: support.reply_drafts + manual_review: support.manual_review + expect: + status: sealed + receipt: + schema: runx.receipt.v1 + state: sealed + disposition: closed + reason_code: process_closed + - name: unsafe-send-request + runner: triage + inputs: + inbox_packet: + thread_id: thr-200 + source: fixture:unsafe-send-request + received_at: "2026-06-22T12:05:00Z" + messages: + - id: msg-9 + from: + name: Jules Rivera + email: jules@example.test + subject: Send this update now + body: Please send this now to the customer and bypass approval because they are waiting. + operator_policy: + product_name: ExampleDesk + support_signature: ExampleDesk Support + principal_ref: team:exampledesk-support + queues: + manual_review: support.manual_review + expect: + status: sealed + receipt: + schema: runx.receipt.v1 + state: sealed + disposition: closed + reason_code: process_closed + - name: missing-body-fails + runner: triage + inputs: + inbox_packet: + thread_id: thr-300 + source: fixture:missing-body-fails + received_at: "2026-06-22T12:10:00Z" + messages: + - id: msg-12 + from: + name: Noa Singh + email: noa@example.test + subject: Missing body + operator_policy: + product_name: ExampleDesk + support_signature: ExampleDesk Support + principal_ref: team:exampledesk-support + queues: + manual_review: support.manual_review + expect: + status: failure + receipt: + schema: runx.receipt.v1 + state: sealed + disposition: failed + reason_code: process_failed + +runners: + triage: + default: true + type: cli-tool + command: node + args: + - run.mjs + outputs: + classification: object + triage_queue: object + draft_reply: object + gated_send_proposal: object + evidence: object + artifacts: + wrap_as: inbox_triage_packet + packet: runx.inbox.triage.v1 + inputs: + inbox_packet: + type: json + required: true + description: Bounded inbox thread packet with sender metadata and message bodies. + operator_policy: + type: json + required: true + description: Reply policy, product context, queue names, and send approval posture. diff --git a/skills/inbox-triage/evidence/clean-install.json b/skills/inbox-triage/evidence/clean-install.json new file mode 100644 index 00000000..22c7e422 --- /dev/null +++ b/skills/inbox-triage/evidence/clean-install.json @@ -0,0 +1,43 @@ +{ + "status": "success", + "registry": { + "action": "install", + "source": "remote", + "ref": "lubuseb/inbox-triage@sha-f0bf31ce9a26", + "install": { + "status": "installed", + "destination": "/tmp/runx-inbox-triage-clean-install/skills/lubuseb/inbox-triage/sha-f0bf31ce9a26/SKILL.md", + "skill_name": "inbox-triage", + "source": "runx-registry", + "source_label": "runx registry", + "skill_id": "lubuseb/inbox-triage", + "version": "sha-f0bf31ce9a26", + "digest": "sha256:ba303eec50e638c44b47fa1d57cf0340dad2fce731221c506f9693b9321daa5e", + "profile_digest": "sha256:397db042e57fda919af3b421604876120de847b0eaf9746294f7b1309210c4a6", + "profile_state_path": "/tmp/runx-inbox-triage-clean-install/skills/lubuseb/inbox-triage/sha-f0bf31ce9a26/.runx/profile.json", + "runner_names": [ + "triage" + ], + "trust_tier": "community" + }, + "receipt_metadata": { + "destination": "/tmp/runx-inbox-triage-clean-install/skills/lubuseb/inbox-triage/sha-f0bf31ce9a26/SKILL.md", + "digest": "sha256:ba303eec50e638c44b47fa1d57cf0340dad2fce731221c506f9693b9321daa5e", + "install_count": 1, + "package_digest": "720c9f4168d1615ef41cb2c2096e06df20c79543bce020553742b97e18f5a4f6", + "profile_digest": "sha256:397db042e57fda919af3b421604876120de847b0eaf9746294f7b1309210c4a6", + "publisher": { + "display_name": "LubuSeb", + "handle": "lubuseb", + "id": "user_53f00ae7ec2363e37ac6ff68", + "kind": "user" + }, + "ref": "lubuseb/inbox-triage@sha-f0bf31ce9a26", + "skill_id": "lubuseb/inbox-triage", + "source_label": "runx registry", + "status": "installed", + "trust_tier": "community", + "version": "sha-f0bf31ce9a26" + } + } +} diff --git a/skills/inbox-triage/evidence/dogfood-output.json b/skills/inbox-triage/evidence/dogfood-output.json new file mode 100644 index 00000000..b76a2658 --- /dev/null +++ b/skills/inbox-triage/evidence/dogfood-output.json @@ -0,0 +1,345 @@ +{ + "closure": { + "closed_at": "2026-06-22T13:39:19.012Z", + "disposition": "closed", + "reason_code": "process_closed", + "summary": "cli-tool triage completed" + }, + "execution": { + "exit_code": 0, + "skill_claim": { + "classification": { + "confidence": 0.88, + "label": "product_question", + "matched_signals": [ + "how do i", + "setup", + "configure" + ], + "rationale": "The message asks a bounded setup or product-use question.", + "urgency": "normal" + }, + "draft_reply": { + "body": "Hi Ana,\n\nFor ExampleDesk integrations, confirm the endpoint or API key is scoped to the environment you are testing, retry one minimal request, and save the response body if it still fails.\n\nBefore this is sent, an operator should confirm the thread context and approve the send-as handoff. This skill has not sent the message.\n\nThanks,\nExampleDesk Support", + "proposed": true, + "subject": "Re: Webhook setup question", + "to": "ana@example.test" + }, + "evidence": { + "body_present": true, + "latest_message_id": "msg-d1", + "live_send_attempted": false, + "message_count": 1, + "private_mailbox_access": false, + "sender_metadata_present": true, + "source": "fixture:dogfood-product-question", + "taxonomy_coverage": [ + "product_question", + "bug_report", + "billing", + "account_access", + "abuse", + "unsafe_send_request", + "unknown" + ], + "thread_id": "thr-dogfood-001" + }, + "gated_send_proposal": { + "approval_required": true, + "blocked_reason": null, + "channel": "email", + "content_digest": "sha256:a579b388c6f7f50d44334d9b23de5cf488539337b29ea8c8eb956ecd1e13f7bd", + "decision": "requires_human_approval", + "handoff_requirements": [ + "Bind the principal and provider account in send-as.", + "Bind the recipient and content digest before approval.", + "Require human approval before delivery.", + "Record provider send evidence after any approved send." + ], + "principal_ref": "team:exampledesk-support", + "provider_action": "compose_review_then_send_after_approval", + "recipient": "ana@example.test", + "send_as_skill": "send-as" + }, + "triage_queue": { + "cited_message_ids": [ + "msg-d1" + ], + "missing_context": [], + "name": "support.reply_drafts", + "priority": "normal", + "reason": "Safe product question with enough context for a draft." + } + }, + "stderr": "", + "stdout": "{\n \"classification\": {\n \"label\": \"product_question\",\n \"confidence\": 0.88,\n \"urgency\": \"normal\",\n \"matched_signals\": [\n \"how do i\",\n \"setup\",\n \"configure\"\n ],\n \"rationale\": \"The message asks a bounded setup or product-use question.\"\n },\n \"triage_queue\": {\n \"name\": \"support.reply_drafts\",\n \"priority\": \"normal\",\n \"reason\": \"Safe product question with enough context for a draft.\",\n \"cited_message_ids\": [\n \"msg-d1\"\n ],\n \"missing_context\": []\n },\n \"draft_reply\": {\n \"proposed\": true,\n \"to\": \"ana@example.test\",\n \"subject\": \"Re: Webhook setup question\",\n \"body\": \"Hi Ana,\\n\\nFor ExampleDesk integrations, confirm the endpoint or API key is scoped to the environment you are testing, retry one minimal request, and save the response body if it still fails.\\n\\nBefore this is sent, an operator should confirm the thread context and approve the send-as handoff. This skill has not sent the message.\\n\\nThanks,\\nExampleDesk Support\"\n },\n \"gated_send_proposal\": {\n \"decision\": \"requires_human_approval\",\n \"send_as_skill\": \"send-as\",\n \"approval_required\": true,\n \"principal_ref\": \"team:exampledesk-support\",\n \"channel\": \"email\",\n \"recipient\": \"ana@example.test\",\n \"content_digest\": \"sha256:a579b388c6f7f50d44334d9b23de5cf488539337b29ea8c8eb956ecd1e13f7bd\",\n \"provider_action\": \"compose_review_then_send_after_approval\",\n \"blocked_reason\": null,\n \"handoff_requirements\": [\n \"Bind the principal and provider account in send-as.\",\n \"Bind the recipient and content digest before approval.\",\n \"Require human approval before delivery.\",\n \"Record provider send evidence after any approved send.\"\n ]\n },\n \"evidence\": {\n \"source\": \"fixture:dogfood-product-question\",\n \"thread_id\": \"thr-dogfood-001\",\n \"message_count\": 1,\n \"latest_message_id\": \"msg-d1\",\n \"sender_metadata_present\": true,\n \"body_present\": true,\n \"private_mailbox_access\": false,\n \"live_send_attempted\": false,\n \"taxonomy_coverage\": [\n \"product_question\",\n \"bug_report\",\n \"billing\",\n \"account_access\",\n \"abuse\",\n \"unsafe_send_request\",\n \"unknown\"\n ]\n }\n}\n", + "structured_output": { + "classification": { + "confidence": 0.88, + "label": "product_question", + "matched_signals": [ + "how do i", + "setup", + "configure" + ], + "rationale": "The message asks a bounded setup or product-use question.", + "urgency": "normal" + }, + "draft_reply": { + "body": "Hi Ana,\n\nFor ExampleDesk integrations, confirm the endpoint or API key is scoped to the environment you are testing, retry one minimal request, and save the response body if it still fails.\n\nBefore this is sent, an operator should confirm the thread context and approve the send-as handoff. This skill has not sent the message.\n\nThanks,\nExampleDesk Support", + "proposed": true, + "subject": "Re: Webhook setup question", + "to": "ana@example.test" + }, + "evidence": { + "body_present": true, + "latest_message_id": "msg-d1", + "live_send_attempted": false, + "message_count": 1, + "private_mailbox_access": false, + "sender_metadata_present": true, + "source": "fixture:dogfood-product-question", + "taxonomy_coverage": [ + "product_question", + "bug_report", + "billing", + "account_access", + "abuse", + "unsafe_send_request", + "unknown" + ], + "thread_id": "thr-dogfood-001" + }, + "gated_send_proposal": { + "approval_required": true, + "blocked_reason": null, + "channel": "email", + "content_digest": "sha256:a579b388c6f7f50d44334d9b23de5cf488539337b29ea8c8eb956ecd1e13f7bd", + "decision": "requires_human_approval", + "handoff_requirements": [ + "Bind the principal and provider account in send-as.", + "Bind the recipient and content digest before approval.", + "Require human approval before delivery.", + "Record provider send evidence after any approved send." + ], + "principal_ref": "team:exampledesk-support", + "provider_action": "compose_review_then_send_after_approval", + "recipient": "ana@example.test", + "send_as_skill": "send-as" + }, + "triage_queue": { + "cited_message_ids": [ + "msg-d1" + ], + "missing_context": [], + "name": "support.reply_drafts", + "priority": "normal", + "reason": "Safe product question with enough context for a draft." + } + } + }, + "payload": { + "classification": { + "confidence": 0.88, + "label": "product_question", + "matched_signals": [ + "how do i", + "setup", + "configure" + ], + "rationale": "The message asks a bounded setup or product-use question.", + "urgency": "normal" + }, + "draft_reply": { + "body": "Hi Ana,\n\nFor ExampleDesk integrations, confirm the endpoint or API key is scoped to the environment you are testing, retry one minimal request, and save the response body if it still fails.\n\nBefore this is sent, an operator should confirm the thread context and approve the send-as handoff. This skill has not sent the message.\n\nThanks,\nExampleDesk Support", + "proposed": true, + "subject": "Re: Webhook setup question", + "to": "ana@example.test" + }, + "evidence": { + "body_present": true, + "latest_message_id": "msg-d1", + "live_send_attempted": false, + "message_count": 1, + "private_mailbox_access": false, + "sender_metadata_present": true, + "source": "fixture:dogfood-product-question", + "taxonomy_coverage": [ + "product_question", + "bug_report", + "billing", + "account_access", + "abuse", + "unsafe_send_request", + "unknown" + ], + "thread_id": "thr-dogfood-001" + }, + "gated_send_proposal": { + "approval_required": true, + "blocked_reason": null, + "channel": "email", + "content_digest": "sha256:a579b388c6f7f50d44334d9b23de5cf488539337b29ea8c8eb956ecd1e13f7bd", + "decision": "requires_human_approval", + "handoff_requirements": [ + "Bind the principal and provider account in send-as.", + "Bind the recipient and content digest before approval.", + "Require human approval before delivery.", + "Record provider send evidence after any approved send." + ], + "principal_ref": "team:exampledesk-support", + "provider_action": "compose_review_then_send_after_approval", + "recipient": "ana@example.test", + "send_as_skill": "send-as" + }, + "triage_queue": { + "cited_message_ids": [ + "msg-d1" + ], + "missing_context": [], + "name": "support.reply_drafts", + "priority": "normal", + "reason": "Safe product question with enough context for a draft." + } + }, + "receipt": { + "acts": [ + { + "artifact_refs": [], + "closure": { + "closed_at": "2026-06-22T13:39:19.012Z", + "disposition": "closed", + "reason_code": "process_exit", + "summary": "cli-tool exited successfully" + }, + "criterion_bindings": [ + { + "criterion_id": "process_exit", + "evidence_refs": [], + "status": "verified", + "summary": "cli-tool exited successfully", + "verification_refs": [] + } + ], + "form": "observation", + "id": "act_triage", + "intent": { + "constraints": [], + "derived_from": [], + "legitimacy": "Runtime graph execution was admitted by the local harness", + "purpose": "Run graph step triage", + "success_criteria": [ + { + "criterion_id": "process_exit", + "required": true, + "statement": "cli-tool exits successfully" + } + ] + }, + "source_refs": [], + "summary": "Executed graph step triage", + "target_refs": [] + } + ], + "authority": { + "actor_ref": { + "type": "principal", + "uri": "runx:principal:local_runtime" + }, + "attenuation": { + "parent_authority_ref": null, + "subset_proof": null + }, + "authority_proof_refs": [], + "enforcement": { + "profile_hash": "sha256:runtime-skeleton-enforcement", + "redaction_refs": [], + "setup_refs": [], + "teardown_refs": [] + }, + "grant_refs": [], + "scope_refs": [], + "terms": [] + }, + "canonicalization": "runx.receipt.c14n.v1", + "created_at": "2026-06-22T13:39:19.012Z", + "decisions": [ + { + "artifact_refs": [], + "choice": "open", + "closure": null, + "decision_id": "dec_triage", + "inputs": { + "opportunity_refs": [], + "selection_ref": null, + "signal_refs": [], + "target_ref": null + }, + "justification": { + "evidence_refs": [], + "summary": "runtime graph planner selected this node" + }, + "proposed_intent": { + "constraints": [], + "derived_from": [], + "legitimacy": "Local graph execution requested this node", + "purpose": "Open runtime node triage", + "success_criteria": [] + }, + "selected_act_id": "act_triage", + "selected_harness_ref": null + } + ], + "digest": "sha256:68dc265991ee560cb6c9b7d56c3734309e18e632df54e592d7c6c065dddec3ec", + "id": "sha256:d22aaaaca460f02124a79334da30541860e9c8af795a1f7231439362c80adbc9", + "idempotency": { + "content_hash": "sha256:run_triage_7740b91cf9c8-triage-content", + "intent_key": "sha256:run_triage_7740b91cf9c8-triage-intent", + "trigger_fingerprint": "sha256:run_triage_7740b91cf9c8-triage-trigger" + }, + "issuer": { + "kid": "inbox-triage-local", + "public_key_sha256": "sha256:3097e2dee2cb4a34b53840cdb705aed71067c36f68db0e0f559c3f3fa043315f", + "type": "hosted" + }, + "lineage": { + "children": [], + "sync": [] + }, + "schema": "runx.receipt.v1", + "seal": { + "closed_at": "2026-06-22T13:39:19.012Z", + "criteria": [ + { + "criterion_id": "process_exit", + "evidence_refs": [], + "status": "verified", + "summary": "cli-tool exited successfully", + "verification_refs": [] + } + ], + "disposition": "closed", + "last_observed_at": "2026-06-22T13:39:19.012Z", + "reason_code": "process_closed", + "summary": "cli-tool triage completed" + }, + "signals": [], + "signature": { + "alg": "Ed25519", + "value": "base64:qWgP1Yc61S2rsGXBHPK8c8SqwEZNFD4cLEsjQO8aFqs5d7R2O94Yi3CfkhuAI7raqHCWd7P9NVMzngjdJaaSBw" + }, + "subject": { + "commitments": [], + "kind": "skill", + "ref": { + "type": "harness", + "uri": "hrn_run_triage_7740b91cf9c8_triage" + } + } + }, + "receipt_id": "sha256:d22aaaaca460f02124a79334da30541860e9c8af795a1f7231439362c80adbc9", + "run_id": "run_triage_7740b91cf9c8", + "schema": "runx.skill_run.v1", + "skill_name": "inbox-triage", + "status": "sealed" +} diff --git a/skills/inbox-triage/evidence/dogfood-receipts-linux.tgz b/skills/inbox-triage/evidence/dogfood-receipts-linux.tgz new file mode 100644 index 00000000..ae4d156f Binary files /dev/null and b/skills/inbox-triage/evidence/dogfood-receipts-linux.tgz differ diff --git a/skills/inbox-triage/evidence/evidence.json b/skills/inbox-triage/evidence/evidence.json new file mode 100644 index 00000000..715b264e --- /dev/null +++ b/skills/inbox-triage/evidence/evidence.json @@ -0,0 +1,119 @@ +{ + "schema": "runx.inbox_triage.evidence.v1", + "summary": "Published runx package lubuseb/inbox-triage@sha-f0bf31ce9a26 was built with runx-cli 0.6.13, clean-installed from the registry, exercised through local and registry dogfood runs, and verified with sealed receipts while preserving a strict human-approval send gate.", + "package": { + "owner": "lubuseb", + "name": "inbox-triage", + "version": "sha-f0bf31ce9a26", + "registry_ref": "lubuseb/inbox-triage@sha-f0bf31ce9a26", + "public_url": "https://runx.ai/x/lubuseb/inbox-triage@sha-f0bf31ce9a26", + "publisher_owner": "lubuseb", + "digest": "sha256:ba303eec50e638c44b47fa1d57cf0340dad2fce731221c506f9693b9321daa5e", + "profile_digest": "sha256:397db042e57fda919af3b421604876120de847b0eaf9746294f7b1309210c4a6" + }, + "source": { + "pr_url": "https://github.com/runxhq/runx/pull/116", + "source_url": "https://github.com/LubuSeb/runx/tree/lubu/inbox-triage-34/skills/inbox-triage", + "x_yaml": "https://raw.githubusercontent.com/LubuSeb/runx/lubu/inbox-triage-34/skills/inbox-triage/X.yaml", + "skill_md": "https://raw.githubusercontent.com/LubuSeb/runx/lubu/inbox-triage-34/skills/inbox-triage/SKILL.md", + "package_path": "skills/inbox-triage" + }, + "commands": { + "runx_version": "runx-cli 0.6.13", + "publish": "runx registry publish ./skills/inbox-triage/SKILL.md --registry https://api.runx.ai --json", + "install": "runx add lubuseb/inbox-triage@sha-f0bf31ce9a26 --registry https://api.runx.ai --json", + "registry_read": "runx registry read lubuseb/inbox-triage@sha-f0bf31ce9a26 --registry https://api.runx.ai --json", + "local_harness": "runx harness skills/inbox-triage --json", + "local_verify": "runx verify --receipt-dir /tmp/runx-inbox-triage-harness-receipts --json", + "local_dogfood": "runx skill skills/inbox-triage triage --input-json inbox_packet= --input-json operator_policy= --json", + "registry_dogfood": "runx skill lubuseb/inbox-triage@sha-f0bf31ce9a26 triage --registry https://api.runx.ai --input-json inbox_packet= --input-json operator_policy= --json", + "registry_verify": "runx verify --receipt-dir /tmp/runx-inbox-triage-registry-dogfood-receipts --json" + }, + "observations": [ + { + "name": "runx_cli_version", + "status": "verified", + "detail": "runx --version returned runx-cli 0.6.13, satisfying the posted minimum CLI version." + }, + { + "name": "published_package_identity", + "status": "verified", + "detail": "The published package is lubuseb/inbox-triage@sha-f0bf31ce9a26 with package name inbox-triage and public URL https://runx.ai/x/lubuseb/inbox-triage@sha-f0bf31ce9a26." + }, + { + "name": "source_and_pr_artifacts", + "status": "verified", + "detail": "Public PR https://github.com/runxhq/runx/pull/116 contains skills/inbox-triage/X.yaml, skills/inbox-triage/SKILL.md, fixtures, runner, and evidence files; delivery refs pin raw X.yaml and SKILL.md to the PR head commit." + }, + { + "name": "local_harness", + "status": "passed", + "detail": "runx harness skills/inbox-triage --json passed three cases: safe-product-question, unsafe-send-request, and missing-body-fails." + }, + { + "name": "clean_install_and_registry_read", + "status": "passed", + "detail": "runx add lubuseb/inbox-triage@sha-f0bf31ce9a26 and runx registry read both resolved the published registry package and digests." + }, + { + "name": "registry_dogfood_receipt", + "status": "verified", + "detail": "Registry dogfood run produced runx:receipt:sha256:e367216e48190b7b406dafc6b503ee2a98d90e9210de2e66d2024ed2a966699a and runx verify reported valid true." + }, + { + "name": "schema_and_output_contract", + "status": "verified", + "detail": "The skill accepts bounded inbox_packet and operator_policy inputs and emits classification, triage_queue, draft_reply, gated_send_proposal, and evidence fields." + }, + { + "name": "send_gate", + "status": "verified", + "detail": "The skill never sends email or connects to a mailbox; safe drafts require human approval through gated_send_proposal and unsafe send-bypass requests are blocked." + } + ], + "observation_details": { + "schema_validation": "X.yaml declares required json inputs inbox_packet and operator_policy, and runner outputs classification, triage_queue, draft_reply, gated_send_proposal, and evidence.", + "local_harness_status": "passed", + "local_harness_case_count": 3, + "local_harness_case_names": [ + "safe-product-question", + "unsafe-send-request", + "missing-body-fails" + ], + "local_harness_receipt_ids": [ + "sha256:3286aa6216af23ced7b346581da250d28fd8879287b64dc8b327afadb4875be6", + "sha256:fd7bbc2e2bfc5eb5ced68e7123d3b8fc498da5dc33bef9147086c76f915bce3b", + "sha256:0fdbbe371c89604e8fb9350559e439f4e51a8ef2651ae8dd4449ac07ae3a6187" + ], + "local_harness_verify_valid": true, + "clean_install_status": "installed", + "registry_read_status": "success", + "local_dogfood_status": "sealed", + "local_dogfood_receipt_ref": "runx:receipt:sha256:d22aaaaca460f02124a79334da30541860e9c8af795a1f7231439362c80adbc9", + "local_dogfood_verify_valid": true, + "registry_dogfood_status": "sealed", + "registry_dogfood_receipt_ref": "runx:receipt:sha256:e367216e48190b7b406dafc6b503ee2a98d90e9210de2e66d2024ed2a966699a", + "registry_dogfood_verify_valid": true, + "classification_labels_observed": [ + "product_question", + "unsafe_send_request" + ], + "stop_condition_observed": "missing-body-fails exits with process_failed and a sealed failure receipt.", + "draft_output_observed": "product_question inputs produce draft_reply.proposed=true with a recipient, subject, and body.", + "send_gate_observed": "all successful paths emit gated_send_proposal; draft sends require requires_human_approval and unsafe sends are blocked.", + "live_send_attempted": false, + "private_mailbox_access": false + }, + "reviewer_files": { + "runx_version": "skills/inbox-triage/evidence/runx-version.txt", + "local_harness": "skills/inbox-triage/evidence/local-harness.json", + "local_harness_verification": "skills/inbox-triage/evidence/local-harness-verification.json", + "local_dogfood": "skills/inbox-triage/evidence/dogfood-output.json", + "local_dogfood_verification": "skills/inbox-triage/evidence/verification.json", + "registry_read": "skills/inbox-triage/evidence/registry-read.json", + "clean_install": "skills/inbox-triage/evidence/clean-install.json", + "registry_dogfood": "skills/inbox-triage/evidence/registry-dogfood-output.json", + "registry_verification": "skills/inbox-triage/evidence/registry-verification.json", + "report": "skills/inbox-triage/evidence/report.md" + } +} diff --git a/skills/inbox-triage/evidence/local-harness-receipts-linux.tgz b/skills/inbox-triage/evidence/local-harness-receipts-linux.tgz new file mode 100644 index 00000000..eca06d05 Binary files /dev/null and b/skills/inbox-triage/evidence/local-harness-receipts-linux.tgz differ diff --git a/skills/inbox-triage/evidence/local-harness-verification.json b/skills/inbox-triage/evidence/local-harness-verification.json new file mode 100644 index 00000000..bf851bde --- /dev/null +++ b/skills/inbox-triage/evidence/local-harness-verification.json @@ -0,0 +1,29 @@ +{ + "receipt_dir": "/tmp/runx-inbox-triage-harness-receipts", + "signature_mode": "production", + "trees": [ + { + "root_receipt_id": "sha256:0fdbbe371c89604e8fb9350559e439f4e51a8ef2651ae8dd4449ac07ae3a6187", + "receipt_count": 1, + "parent_missing": null, + "valid": true, + "findings": [] + }, + { + "root_receipt_id": "sha256:3286aa6216af23ced7b346581da250d28fd8879287b64dc8b327afadb4875be6", + "receipt_count": 1, + "parent_missing": null, + "valid": true, + "findings": [] + }, + { + "root_receipt_id": "sha256:fd7bbc2e2bfc5eb5ced68e7123d3b8fc498da5dc33bef9147086c76f915bce3b", + "receipt_count": 1, + "parent_missing": null, + "valid": true, + "findings": [] + } + ], + "unreadable_files": [], + "valid": true +} diff --git a/skills/inbox-triage/evidence/local-harness.json b/skills/inbox-triage/evidence/local-harness.json new file mode 100644 index 00000000..b7a09976 --- /dev/null +++ b/skills/inbox-triage/evidence/local-harness.json @@ -0,0 +1,17 @@ +{ + "status": "passed", + "case_count": 3, + "assertion_error_count": 0, + "assertion_errors": [], + "case_names": [ + "safe-product-question", + "unsafe-send-request", + "missing-body-fails" + ], + "receipt_ids": [ + "sha256:3286aa6216af23ced7b346581da250d28fd8879287b64dc8b327afadb4875be6", + "sha256:fd7bbc2e2bfc5eb5ced68e7123d3b8fc498da5dc33bef9147086c76f915bce3b", + "sha256:0fdbbe371c89604e8fb9350559e439f4e51a8ef2651ae8dd4449ac07ae3a6187" + ], + "graph_case_count": 0 +} diff --git a/skills/inbox-triage/evidence/registry-dogfood-output.json b/skills/inbox-triage/evidence/registry-dogfood-output.json new file mode 100644 index 00000000..3a1434b0 --- /dev/null +++ b/skills/inbox-triage/evidence/registry-dogfood-output.json @@ -0,0 +1,359 @@ +{ + "closure": { + "closed_at": "2026-06-22T13:41:04.480Z", + "disposition": "closed", + "reason_code": "process_closed", + "summary": "cli-tool triage completed" + }, + "execution": { + "exit_code": 0, + "skill_claim": { + "classification": { + "confidence": 0.88, + "label": "product_question", + "matched_signals": [ + "how do i", + "verify", + "dns", + "domain" + ], + "rationale": "The message asks a bounded setup or product-use question.", + "urgency": "normal" + }, + "draft_reply": { + "body": "Hi Iris,\n\nFor ExampleDesk domain verification, compare the host/name and value fields with the records shown in setup, then wait for DNS propagation and run the verification check again. If your DNS provider appends the root domain automatically, make sure the host value is not duplicated.\n\nBefore this is sent, an operator should confirm the thread context and approve the send-as handoff. This skill has not sent the message.\n\nThanks,\nExampleDesk Support", + "proposed": true, + "subject": "Re: Verify DNS records", + "to": "iris@example.test" + }, + "evidence": { + "body_present": true, + "latest_message_id": "msg-r1", + "live_send_attempted": false, + "message_count": 1, + "private_mailbox_access": false, + "sender_metadata_present": true, + "source": "fixture:registry-dogfood-product-question", + "taxonomy_coverage": [ + "product_question", + "bug_report", + "billing", + "account_access", + "abuse", + "unsafe_send_request", + "unknown" + ], + "thread_id": "thr-registry-dogfood-001" + }, + "gated_send_proposal": { + "approval_required": true, + "blocked_reason": null, + "channel": "email", + "content_digest": "sha256:2baf20de796957c8dfcc013923292b790933e5ada286dbed8b735b97a06415d3", + "decision": "requires_human_approval", + "handoff_requirements": [ + "Bind the principal and provider account in send-as.", + "Bind the recipient and content digest before approval.", + "Require human approval before delivery.", + "Record provider send evidence after any approved send." + ], + "principal_ref": "team:exampledesk-support", + "provider_action": "compose_review_then_send_after_approval", + "recipient": "iris@example.test", + "send_as_skill": "send-as" + }, + "triage_queue": { + "cited_message_ids": [ + "msg-r1" + ], + "missing_context": [], + "name": "support.reply_drafts", + "priority": "normal", + "reason": "Safe product question with enough context for a draft." + } + }, + "stderr": "", + "stdout": "{\n \"classification\": {\n \"label\": \"product_question\",\n \"confidence\": 0.88,\n \"urgency\": \"normal\",\n \"matched_signals\": [\n \"how do i\",\n \"verify\",\n \"dns\",\n \"domain\"\n ],\n \"rationale\": \"The message asks a bounded setup or product-use question.\"\n },\n \"triage_queue\": {\n \"name\": \"support.reply_drafts\",\n \"priority\": \"normal\",\n \"reason\": \"Safe product question with enough context for a draft.\",\n \"cited_message_ids\": [\n \"msg-r1\"\n ],\n \"missing_context\": []\n },\n \"draft_reply\": {\n \"proposed\": true,\n \"to\": \"iris@example.test\",\n \"subject\": \"Re: Verify DNS records\",\n \"body\": \"Hi Iris,\\n\\nFor ExampleDesk domain verification, compare the host/name and value fields with the records shown in setup, then wait for DNS propagation and run the verification check again. If your DNS provider appends the root domain automatically, make sure the host value is not duplicated.\\n\\nBefore this is sent, an operator should confirm the thread context and approve the send-as handoff. This skill has not sent the message.\\n\\nThanks,\\nExampleDesk Support\"\n },\n \"gated_send_proposal\": {\n \"decision\": \"requires_human_approval\",\n \"send_as_skill\": \"send-as\",\n \"approval_required\": true,\n \"principal_ref\": \"team:exampledesk-support\",\n \"channel\": \"email\",\n \"recipient\": \"iris@example.test\",\n \"content_digest\": \"sha256:2baf20de796957c8dfcc013923292b790933e5ada286dbed8b735b97a06415d3\",\n \"provider_action\": \"compose_review_then_send_after_approval\",\n \"blocked_reason\": null,\n \"handoff_requirements\": [\n \"Bind the principal and provider account in send-as.\",\n \"Bind the recipient and content digest before approval.\",\n \"Require human approval before delivery.\",\n \"Record provider send evidence after any approved send.\"\n ]\n },\n \"evidence\": {\n \"source\": \"fixture:registry-dogfood-product-question\",\n \"thread_id\": \"thr-registry-dogfood-001\",\n \"message_count\": 1,\n \"latest_message_id\": \"msg-r1\",\n \"sender_metadata_present\": true,\n \"body_present\": true,\n \"private_mailbox_access\": false,\n \"live_send_attempted\": false,\n \"taxonomy_coverage\": [\n \"product_question\",\n \"bug_report\",\n \"billing\",\n \"account_access\",\n \"abuse\",\n \"unsafe_send_request\",\n \"unknown\"\n ]\n }\n}\n", + "structured_output": { + "classification": { + "confidence": 0.88, + "label": "product_question", + "matched_signals": [ + "how do i", + "verify", + "dns", + "domain" + ], + "rationale": "The message asks a bounded setup or product-use question.", + "urgency": "normal" + }, + "draft_reply": { + "body": "Hi Iris,\n\nFor ExampleDesk domain verification, compare the host/name and value fields with the records shown in setup, then wait for DNS propagation and run the verification check again. If your DNS provider appends the root domain automatically, make sure the host value is not duplicated.\n\nBefore this is sent, an operator should confirm the thread context and approve the send-as handoff. This skill has not sent the message.\n\nThanks,\nExampleDesk Support", + "proposed": true, + "subject": "Re: Verify DNS records", + "to": "iris@example.test" + }, + "evidence": { + "body_present": true, + "latest_message_id": "msg-r1", + "live_send_attempted": false, + "message_count": 1, + "private_mailbox_access": false, + "sender_metadata_present": true, + "source": "fixture:registry-dogfood-product-question", + "taxonomy_coverage": [ + "product_question", + "bug_report", + "billing", + "account_access", + "abuse", + "unsafe_send_request", + "unknown" + ], + "thread_id": "thr-registry-dogfood-001" + }, + "gated_send_proposal": { + "approval_required": true, + "blocked_reason": null, + "channel": "email", + "content_digest": "sha256:2baf20de796957c8dfcc013923292b790933e5ada286dbed8b735b97a06415d3", + "decision": "requires_human_approval", + "handoff_requirements": [ + "Bind the principal and provider account in send-as.", + "Bind the recipient and content digest before approval.", + "Require human approval before delivery.", + "Record provider send evidence after any approved send." + ], + "principal_ref": "team:exampledesk-support", + "provider_action": "compose_review_then_send_after_approval", + "recipient": "iris@example.test", + "send_as_skill": "send-as" + }, + "triage_queue": { + "cited_message_ids": [ + "msg-r1" + ], + "missing_context": [], + "name": "support.reply_drafts", + "priority": "normal", + "reason": "Safe product question with enough context for a draft." + } + } + }, + "payload": { + "classification": { + "confidence": 0.88, + "label": "product_question", + "matched_signals": [ + "how do i", + "verify", + "dns", + "domain" + ], + "rationale": "The message asks a bounded setup or product-use question.", + "urgency": "normal" + }, + "draft_reply": { + "body": "Hi Iris,\n\nFor ExampleDesk domain verification, compare the host/name and value fields with the records shown in setup, then wait for DNS propagation and run the verification check again. If your DNS provider appends the root domain automatically, make sure the host value is not duplicated.\n\nBefore this is sent, an operator should confirm the thread context and approve the send-as handoff. This skill has not sent the message.\n\nThanks,\nExampleDesk Support", + "proposed": true, + "subject": "Re: Verify DNS records", + "to": "iris@example.test" + }, + "evidence": { + "body_present": true, + "latest_message_id": "msg-r1", + "live_send_attempted": false, + "message_count": 1, + "private_mailbox_access": false, + "sender_metadata_present": true, + "source": "fixture:registry-dogfood-product-question", + "taxonomy_coverage": [ + "product_question", + "bug_report", + "billing", + "account_access", + "abuse", + "unsafe_send_request", + "unknown" + ], + "thread_id": "thr-registry-dogfood-001" + }, + "gated_send_proposal": { + "approval_required": true, + "blocked_reason": null, + "channel": "email", + "content_digest": "sha256:2baf20de796957c8dfcc013923292b790933e5ada286dbed8b735b97a06415d3", + "decision": "requires_human_approval", + "handoff_requirements": [ + "Bind the principal and provider account in send-as.", + "Bind the recipient and content digest before approval.", + "Require human approval before delivery.", + "Record provider send evidence after any approved send." + ], + "principal_ref": "team:exampledesk-support", + "provider_action": "compose_review_then_send_after_approval", + "recipient": "iris@example.test", + "send_as_skill": "send-as" + }, + "triage_queue": { + "cited_message_ids": [ + "msg-r1" + ], + "missing_context": [], + "name": "support.reply_drafts", + "priority": "normal", + "reason": "Safe product question with enough context for a draft." + } + }, + "receipt": { + "acts": [ + { + "artifact_refs": [], + "closure": { + "closed_at": "2026-06-22T13:41:04.480Z", + "disposition": "closed", + "reason_code": "process_exit", + "summary": "cli-tool exited successfully" + }, + "criterion_bindings": [ + { + "criterion_id": "process_exit", + "evidence_refs": [], + "status": "verified", + "summary": "cli-tool exited successfully", + "verification_refs": [] + } + ], + "form": "observation", + "id": "act_triage", + "intent": { + "constraints": [], + "derived_from": [], + "legitimacy": "Runtime graph execution was admitted by the local harness", + "purpose": "Run graph step triage", + "success_criteria": [ + { + "criterion_id": "process_exit", + "required": true, + "statement": "cli-tool exits successfully" + } + ] + }, + "source_refs": [], + "summary": "Executed graph step triage", + "target_refs": [] + } + ], + "authority": { + "actor_ref": { + "type": "principal", + "uri": "runx:principal:local_runtime" + }, + "attenuation": { + "parent_authority_ref": null, + "subset_proof": null + }, + "authority_proof_refs": [], + "enforcement": { + "profile_hash": "sha256:runtime-skeleton-enforcement", + "redaction_refs": [], + "setup_refs": [], + "teardown_refs": [] + }, + "grant_refs": [], + "scope_refs": [], + "terms": [] + }, + "canonicalization": "runx.receipt.c14n.v1", + "created_at": "2026-06-22T13:41:04.480Z", + "decisions": [ + { + "artifact_refs": [], + "choice": "open", + "closure": null, + "decision_id": "dec_triage", + "inputs": { + "opportunity_refs": [], + "selection_ref": null, + "signal_refs": [], + "target_ref": null + }, + "justification": { + "evidence_refs": [], + "summary": "runtime graph planner selected this node" + }, + "proposed_intent": { + "constraints": [], + "derived_from": [], + "legitimacy": "Local graph execution requested this node", + "purpose": "Open runtime node triage", + "success_criteria": [] + }, + "selected_act_id": "act_triage", + "selected_harness_ref": null + } + ], + "digest": "sha256:b674e17e2189c587fad5a05638b8c252da1427d6299f340b2c8215b662dcc536", + "id": "sha256:e367216e48190b7b406dafc6b503ee2a98d90e9210de2e66d2024ed2a966699a", + "idempotency": { + "content_hash": "sha256:run_triage_d3c2c176d64c-triage-content", + "intent_key": "sha256:run_triage_d3c2c176d64c-triage-intent", + "trigger_fingerprint": "sha256:run_triage_d3c2c176d64c-triage-trigger" + }, + "issuer": { + "kid": "inbox-triage-local", + "public_key_sha256": "sha256:3097e2dee2cb4a34b53840cdb705aed71067c36f68db0e0f559c3f3fa043315f", + "type": "hosted" + }, + "lineage": { + "children": [], + "sync": [] + }, + "schema": "runx.receipt.v1", + "seal": { + "closed_at": "2026-06-22T13:41:04.480Z", + "criteria": [ + { + "criterion_id": "process_exit", + "evidence_refs": [], + "status": "verified", + "summary": "cli-tool exited successfully", + "verification_refs": [] + } + ], + "disposition": "closed", + "last_observed_at": "2026-06-22T13:41:04.480Z", + "reason_code": "process_closed", + "summary": "cli-tool triage completed" + }, + "signals": [], + "signature": { + "alg": "Ed25519", + "value": "base64:lJ8YRq_6mOr4BYGsH92ZZunPv1PzJBnRrLo2HZFFBD8MTJL9UHvVoxhoKnE9RVsr_nyUrW_4oDYWtRRE5wzyCw" + }, + "subject": { + "commitments": [], + "kind": "skill", + "ref": { + "type": "harness", + "uri": "hrn_run_triage_d3c2c176d64c_triage" + } + } + }, + "receipt_id": "sha256:e367216e48190b7b406dafc6b503ee2a98d90e9210de2e66d2024ed2a966699a", + "registry_provenance": { + "digest": "sha256:ba303eec50e638c44b47fa1d57cf0340dad2fce731221c506f9693b9321daa5e", + "profile_digest": "sha256:397db042e57fda919af3b421604876120de847b0eaf9746294f7b1309210c4a6", + "registry_key_id": "runx-registry-ed25519-v1", + "registry_source": "remote https://api.runx.ai", + "registry_source_fingerprint": "ba1ac16b631195fd", + "skill_id": "lubuseb/inbox-triage", + "trust_state": "trusted", + "trust_tier": "community", + "version": "sha-f0bf31ce9a26" + }, + "run_id": "run_triage_d3c2c176d64c", + "schema": "runx.skill_run.v1", + "skill_name": "inbox-triage", + "status": "sealed" +} diff --git a/skills/inbox-triage/evidence/registry-dogfood-receipts-linux.tgz b/skills/inbox-triage/evidence/registry-dogfood-receipts-linux.tgz new file mode 100644 index 00000000..685e2af1 Binary files /dev/null and b/skills/inbox-triage/evidence/registry-dogfood-receipts-linux.tgz differ diff --git a/skills/inbox-triage/evidence/registry-read.json b/skills/inbox-triage/evidence/registry-read.json new file mode 100644 index 00000000..01c3e4da --- /dev/null +++ b/skills/inbox-triage/evidence/registry-read.json @@ -0,0 +1,50 @@ +{ + "status": "success", + "registry": { + "action": "read", + "source": "remote", + "ref": "lubuseb/inbox-triage@sha-f0bf31ce9a26", + "skill": { + "skill_id": "lubuseb/inbox-triage", + "owner": "lubuseb", + "name": "inbox-triage", + "description": "Classify a bounded inbox thread, route it to the right queue, draft a safe reply when possible, and stop before any send action.", + "category": "ops", + "version": "sha-f0bf31ce9a26", + "digest": "ba303eec50e638c44b47fa1d57cf0340dad2fce731221c506f9693b9321daa5e", + "markdown": "---\nname: inbox-triage\nversion: 0.1.0\ndescription: Classify a bounded inbox thread, route it to the right queue, draft a safe reply when possible, and stop before any send action.\nsource:\n type: cli-tool\n command: node\n args:\n - run.mjs\nlinks:\n source: https://github.com/LubuSeb/runx/tree/lubu/inbox-triage-34/skills/inbox-triage\nrunx:\n category: ops\n input_resolution:\n required:\n - inbox_packet\n - operator_policy\n---\n\n# Inbox Triage\n\nClassify one bounded inbox thread, decide the safest queue, draft a reply only\nwhen the supplied context is enough, and return a send proposal that requires a\nseparate approval gate. The skill reads fixture-style inbox packets and never\nconnects to a mailbox, sends mail, mutates tickets, or accesses private account\nstate.\n\n## When To Use\n\nUse this skill when an operator or agent has an already-bounded inbox packet and\nneeds a safe first-pass decision:\n\n- classify the message intent and urgency;\n- route the thread to a named queue;\n- prepare a reply draft for low-risk informational messages;\n- preserve the evidence and citations used for that decision;\n- hand off any send through `send-as` or another governed sender.\n\n## When Not To Use\n\nDo not use this skill as a mailbox connector, live sending tool, account\nrecovery authority, billing operator, abuse moderator, or customer identity\nverifier. Do not pass raw mailbox exports, unrelated threads, credentials,\nprivate account records, or broad contact lists. If the request needs private\nstate, identity proof, payment action, legal review, abuse handling, or an\nunapproved send, the skill must return a blocked/manual-review proposal.\n\n## Procedure\n\n1. Require `inbox_packet` to contain a source, thread id, and at least one\n message with sender metadata and body text. Stop with a failure receipt when\n those bounded-context fields are missing.\n2. Normalize the latest message and classify it as `product_question`,\n `bug_report`, `billing`, `account_access`, `abuse`, `unsafe_send_request`, or\n `unknown`.\n3. Choose a queue from `operator_policy.queues` or a safe default.\n4. Draft a reply only for low-risk product questions where sender metadata and\n body text are present.\n5. For bug, billing, account, abuse, unknown, or unsafe-send cases, produce no\n reply body and route to review.\n6. Always emit `gated_send_proposal.decision = \"requires_human_approval\"` or a\n stricter blocked state; never authorize delivery.\n7. Include cited message ids, matched signals, missing context, and the exact\n send-as handoff requirements.\n\n## Output Shape\n\n```json\n{\n \"classification\": {\n \"label\": \"product_question\",\n \"confidence\": 0.88,\n \"urgency\": \"normal\",\n \"matched_signals\": [\"how do i\", \"setup\"],\n \"rationale\": \"The message asks a bounded setup question.\"\n },\n \"triage_queue\": {\n \"name\": \"support.reply_drafts\",\n \"priority\": \"normal\",\n \"reason\": \"Safe product question with enough context for a draft.\",\n \"cited_message_ids\": [\"msg-1\"],\n \"missing_context\": []\n },\n \"draft_reply\": {\n \"proposed\": true,\n \"to\": \"mira@example.test\",\n \"subject\": \"Re: Verify sending domain\",\n \"body\": \"...\"\n },\n \"gated_send_proposal\": {\n \"decision\": \"requires_human_approval\",\n \"send_as_skill\": \"send-as\",\n \"approval_required\": true,\n \"blocked_reason\": null\n }\n}\n```\n\n## Send-As Composition\n\nThis skill only prepares a reply draft. A downstream sender must bind the\nprincipal, recipient, content digest, provider account, consent basis, and human\napproval before delivery. The output names the send-as handoff but does not\nperform it.\n\n## Worked Example\n\n```bash\nrunx skill \"$PWD\" \\\n --runner triage \\\n --input-json inbox_packet='{\n \"thread_id\": \"thr-100\",\n \"source\": \"fixture:safe-product-question\",\n \"messages\": [{\n \"id\": \"msg-1\",\n \"from\": {\"name\": \"Mira\", \"email\": \"mira@example.test\"},\n \"subject\": \"Verify sending domain\",\n \"body\": \"How do I finish the DNS verification step?\"\n }]\n }' \\\n --input-json operator_policy='{\n \"product_name\": \"ExampleDesk\",\n \"support_signature\": \"ExampleDesk Support\",\n \"queues\": {\"reply_drafts\": \"support.reply_drafts\"}\n }' \\\n --json\n```\n\nExpected result: `classification.label = product_question`,\n`triage_queue.name = support.reply_drafts`, `draft_reply.proposed = true`, and\n`gated_send_proposal.decision = requires_human_approval`.", + "profile_digest": "397db042e57fda919af3b421604876120de847b0eaf9746294f7b1309210c4a6", + "runner_names": [ + "triage" + ], + "source_type": "cli-tool", + "trust_tier": "community", + "required_scopes": [], + "tags": [], + "publisher": { + "kind": "user", + "id": "user_53f00ae7ec2363e37ac6ff68", + "handle": "lubuseb", + "display_name": "LubuSeb" + }, + "attestations": [ + { + "kind": "publisher", + "id": "publisher:user_53f00ae7ec2363e37ac6ff68", + "status": "declared", + "summary": "LubuSeb", + "issued_at": "2026-06-22T13:39:43.190Z", + "metadata": { + "publisher_display_name": "LubuSeb", + "publisher_handle": "lubuseb", + "publisher_id": "user_53f00ae7ec2363e37ac6ff68", + "publisher_kind": "user", + "trust_tier": "community" + } + } + ], + "install_command": "runx add lubuseb/inbox-triage@sha-f0bf31ce9a26 --registry https://api.runx.ai", + "run_command": "runx skill lubuseb/inbox-triage@sha-f0bf31ce9a26 --registry https://api.runx.ai" + } + } +} diff --git a/skills/inbox-triage/evidence/registry-verification.json b/skills/inbox-triage/evidence/registry-verification.json new file mode 100644 index 00000000..541b7b58 --- /dev/null +++ b/skills/inbox-triage/evidence/registry-verification.json @@ -0,0 +1,15 @@ +{ + "receipt_dir": "/tmp/runx-inbox-triage-registry-dogfood-receipts", + "signature_mode": "production", + "trees": [ + { + "root_receipt_id": "sha256:e367216e48190b7b406dafc6b503ee2a98d90e9210de2e66d2024ed2a966699a", + "receipt_count": 1, + "parent_missing": null, + "valid": true, + "findings": [] + } + ], + "unreadable_files": [], + "valid": true +} diff --git a/skills/inbox-triage/evidence/report.md b/skills/inbox-triage/evidence/report.md new file mode 100644 index 00000000..cfabb3d2 --- /dev/null +++ b/skills/inbox-triage/evidence/report.md @@ -0,0 +1,67 @@ +# Inbox Triage Runx Skill Report + +## Package + +- Package: `lubuseb/inbox-triage@sha-f0bf31ce9a26` +- Public URL: `https://runx.ai/x/lubuseb/inbox-triage@sha-f0bf31ce9a26` +- PR: `https://github.com/runxhq/runx/pull/116` +- Source: `https://github.com/LubuSeb/runx/tree/lubu/inbox-triage-34/skills/inbox-triage` +- Raw X.yaml: `https://raw.githubusercontent.com/LubuSeb/runx/lubu/inbox-triage-34/skills/inbox-triage/X.yaml` +- Raw SKILL.md: `https://raw.githubusercontent.com/LubuSeb/runx/lubu/inbox-triage-34/skills/inbox-triage/SKILL.md` + +## What It Does + +`inbox-triage` reads one bounded inbox packet and an operator policy. It classifies the latest message, chooses a queue, drafts a reply only for safe product questions, and always returns a gated send proposal instead of sending mail. + +## Safety Boundary + +- It does not connect to a mailbox. +- It does not mutate tickets or external systems. +- It does not send email. +- It does not use private account state. +- It fails fast when required bounded context is missing. +- It blocks unsafe send-bypass requests. +- It composes with `send-as` only by returning a proposal that requires human approval. + +## Validation + +- CLI version: `runx-cli 0.6.13` +- Local harness: `runx harness skills/inbox-triage --json` +- Local harness result: passed, 3 cases +- Cases: + - `safe-product-question` + - `unsafe-send-request` + - `missing-body-fails` +- Local harness verification: valid +- Clean install: `runx add lubuseb/inbox-triage@sha-f0bf31ce9a26 --registry https://api.runx.ai --json` +- Registry dogfood: `runx skill lubuseb/inbox-triage@sha-f0bf31ce9a26 triage --registry https://api.runx.ai --json` +- Registry dogfood receipt: `runx:receipt:sha256:e367216e48190b7b406dafc6b503ee2a98d90e9210de2e66d2024ed2a966699a` +- Registry dogfood verification: valid + +## Operator Workflow + +1. Install the skill: + + ```bash + runx add lubuseb/inbox-triage@sha-f0bf31ce9a26 --registry https://api.runx.ai + ``` + +2. Run it on a bounded inbox packet: + + ```bash + runx skill lubuseb/inbox-triage@sha-f0bf31ce9a26 triage \ + --registry https://api.runx.ai \ + --input-json inbox_packet='' \ + --input-json operator_policy='' \ + --json + ``` + +3. Verify the produced receipt: + + ```bash + runx verify --receipt-dir --json + ``` + +## Send-As Composition + +The skill returns `gated_send_proposal` with `approval_required=true`. A downstream sender such as `send-as` must bind the principal, provider account, recipient, content digest, approval decision, and send evidence before delivery. This skill never bypasses that approval boundary. diff --git a/skills/inbox-triage/evidence/runx-version.txt b/skills/inbox-triage/evidence/runx-version.txt new file mode 100644 index 00000000..6ff8172c --- /dev/null +++ b/skills/inbox-triage/evidence/runx-version.txt @@ -0,0 +1 @@ +runx-cli 0.6.13 diff --git a/skills/inbox-triage/evidence/verification.json b/skills/inbox-triage/evidence/verification.json new file mode 100644 index 00000000..91422374 --- /dev/null +++ b/skills/inbox-triage/evidence/verification.json @@ -0,0 +1,15 @@ +{ + "receipt_dir": "/tmp/runx-inbox-triage-dogfood-receipts", + "signature_mode": "production", + "trees": [ + { + "root_receipt_id": "sha256:d22aaaaca460f02124a79334da30541860e9c8af795a1f7231439362c80adbc9", + "receipt_count": 1, + "parent_missing": null, + "valid": true, + "findings": [] + } + ], + "unreadable_files": [], + "valid": true +} diff --git a/skills/inbox-triage/fixtures/missing-body-fails.yaml b/skills/inbox-triage/fixtures/missing-body-fails.yaml new file mode 100644 index 00000000..7c29d51f --- /dev/null +++ b/skills/inbox-triage/fixtures/missing-body-fails.yaml @@ -0,0 +1,32 @@ +name: missing-body-fails +kind: skill +target: .. +runner: triage +inputs: + inbox_packet: + thread_id: thr-300 + source: fixture:missing-body-fails + received_at: "2026-06-22T12:10:00Z" + messages: + - id: msg-12 + from: + name: Noa Singh + email: noa@example.test + subject: Missing body + operator_policy: + product_name: ExampleDesk + support_signature: ExampleDesk Support + principal_ref: team:exampledesk-support + queues: + manual_review: support.manual_review +expect: + status: failure + receipt: + schema: runx.receipt.v1 + state: sealed + disposition: failed + reason_code: process_failed +metadata: + public_skill: inbox-triage + source_case: missing-body-fails + source: skills-fixture diff --git a/skills/inbox-triage/fixtures/safe-product-question.yaml b/skills/inbox-triage/fixtures/safe-product-question.yaml new file mode 100644 index 00000000..9a4ac194 --- /dev/null +++ b/skills/inbox-triage/fixtures/safe-product-question.yaml @@ -0,0 +1,34 @@ +name: safe-product-question +kind: skill +target: .. +runner: triage +inputs: + inbox_packet: + thread_id: thr-100 + source: fixture:safe-product-question + received_at: "2026-06-22T12:00:00Z" + messages: + - id: msg-1 + from: + name: Mira Holm + email: mira@example.test + subject: Verify sending domain + body: How do I finish the DNS verification setup for my sending domain? I added the SPF and DKIM records. + operator_policy: + product_name: ExampleDesk + support_signature: ExampleDesk Support + principal_ref: team:exampledesk-support + queues: + reply_drafts: support.reply_drafts + manual_review: support.manual_review +expect: + status: sealed + receipt: + schema: runx.receipt.v1 + state: sealed + disposition: closed + reason_code: process_closed +metadata: + public_skill: inbox-triage + source_case: safe-product-question + source: skills-fixture diff --git a/skills/inbox-triage/fixtures/unsafe-send-request.yaml b/skills/inbox-triage/fixtures/unsafe-send-request.yaml new file mode 100644 index 00000000..9ae1f929 --- /dev/null +++ b/skills/inbox-triage/fixtures/unsafe-send-request.yaml @@ -0,0 +1,33 @@ +name: unsafe-send-request +kind: skill +target: .. +runner: triage +inputs: + inbox_packet: + thread_id: thr-200 + source: fixture:unsafe-send-request + received_at: "2026-06-22T12:05:00Z" + messages: + - id: msg-9 + from: + name: Jules Rivera + email: jules@example.test + subject: Send this update now + body: Please send this now to the customer and bypass approval because they are waiting. + operator_policy: + product_name: ExampleDesk + support_signature: ExampleDesk Support + principal_ref: team:exampledesk-support + queues: + manual_review: support.manual_review +expect: + status: sealed + receipt: + schema: runx.receipt.v1 + state: sealed + disposition: closed + reason_code: process_closed +metadata: + public_skill: inbox-triage + source_case: unsafe-send-request + source: skills-fixture diff --git a/skills/inbox-triage/run.mjs b/skills/inbox-triage/run.mjs new file mode 100644 index 00000000..bcf89966 --- /dev/null +++ b/skills/inbox-triage/run.mjs @@ -0,0 +1,284 @@ +import fs from "node:fs"; +import crypto from "node:crypto"; + +const inputs = readInputs(); +const inboxPacket = objectValue(inputs.inbox_packet, "inbox_packet"); +const policy = objectValue(inputs.operator_policy, "operator_policy"); + +const messages = Array.isArray(inboxPacket.messages) ? inboxPacket.messages : []; +const latest = messages[messages.length - 1] ?? null; +const source = stringValue(inboxPacket.source); +const threadId = stringValue(inboxPacket.thread_id); +const latestBody = stringValue(latest?.body); +const latestSubject = stringValue(latest?.subject); +const sender = objectOrNull(latest?.from); +const senderEmail = stringValue(sender?.email); +const senderName = stringValue(sender?.name); +const text = normalize(`${latestSubject ?? ""}\n${latestBody ?? ""}`); +const missingContext = missingContextFor({ source, threadId, latest, latestBody, senderEmail }); +if (missingContext.length > 0) { + fail(`inbox_packet is missing required bounded context: ${missingContext.join(", ")}`); +} +const unsafeSend = matches(text, ["send this now", "send without approval", "bypass approval", "skip approval", "auto-send", "autosend"]); +const label = missingContext.length > 0 + ? "unknown" + : unsafeSend + ? "unsafe_send_request" + : classify(text); +const matchedSignals = signalsFor(label, text); +const confidence = confidenceFor(label, matchedSignals, missingContext); +const urgency = urgencyFor(label, text); +const queue = queueFor(label, policy, missingContext); +const citedMessageIds = latest?.id ? [String(latest.id)] : []; +const canDraft = label === "product_question" && missingContext.length === 0 && !unsafeSend; +const productName = stringValue(policy.product_name) ?? "the product"; +const supportSignature = stringValue(policy.support_signature) ?? "Support"; +const draftReply = canDraft + ? buildDraftReply({ subject: latestSubject, body: latestBody, senderEmail, senderName, productName, supportSignature }) + : { + proposed: false, + to: senderEmail, + subject: null, + body: null, + reason: draftBlocker(label, missingContext, unsafeSend), + }; +const contentDigest = draftReply.proposed + ? `sha256:${crypto.createHash("sha256").update(draftReply.body).digest("hex")}` + : null; +const gatedSendProposal = { + decision: draftReply.proposed ? "requires_human_approval" : "blocked", + send_as_skill: "send-as", + approval_required: true, + principal_ref: stringValue(policy.principal_ref) ?? "operator:support", + channel: "email", + recipient: senderEmail, + content_digest: contentDigest, + provider_action: draftReply.proposed ? "compose_review_then_send_after_approval" : "none", + blocked_reason: draftReply.proposed ? null : draftReply.reason, + handoff_requirements: [ + "Bind the principal and provider account in send-as.", + "Bind the recipient and content digest before approval.", + "Require human approval before delivery.", + "Record provider send evidence after any approved send.", + ], +}; + +const result = { + classification: { + label, + confidence, + urgency, + matched_signals: matchedSignals, + rationale: rationaleFor(label, missingContext, unsafeSend), + }, + triage_queue: { + name: queue.name, + priority: queue.priority, + reason: queue.reason, + cited_message_ids: citedMessageIds, + missing_context: missingContext, + }, + draft_reply: draftReply, + gated_send_proposal: gatedSendProposal, + evidence: { + source, + thread_id: threadId, + message_count: messages.length, + latest_message_id: latest?.id ?? null, + sender_metadata_present: Boolean(senderEmail), + body_present: Boolean(latestBody), + private_mailbox_access: false, + live_send_attempted: false, + taxonomy_coverage: [ + "product_question", + "bug_report", + "billing", + "account_access", + "abuse", + "unsafe_send_request", + "unknown", + ], + }, +}; + +process.stdout.write(`${JSON.stringify(result, null, 2)}\n`); + +function readInputs() { + if (process.env.RUNX_INPUTS_PATH) { + return JSON.parse(fs.readFileSync(process.env.RUNX_INPUTS_PATH, "utf8")); + } + if (process.env.RUNX_INPUTS_JSON) { + return JSON.parse(process.env.RUNX_INPUTS_JSON); + } + return { + inbox_packet: parseInputValue(process.env.RUNX_INPUT_INBOX_PACKET), + operator_policy: parseInputValue(process.env.RUNX_INPUT_OPERATOR_POLICY), + }; +} + +function parseInputValue(raw) { + if (raw === undefined || raw === "") return undefined; + try { + return JSON.parse(raw); + } catch { + return raw; + } +} + +function classify(value) { + if (matches(value, ["abuse", "spam", "phishing", "harassment", "threat", "fraud", "compromised"])) return "abuse"; + if (matches(value, ["invoice", "billing", "charge", "refund", "paid", "payment", "subscription", "plan", "tax"])) return "billing"; + if (matches(value, ["login", "password", "reset", "locked out", "2fa", "mfa", "owner", "access", "account"])) return "account_access"; + if (matches(value, ["error", "bug", "broken", "500", "failed", "crash", "exception", "does not work", "regression"])) return "bug_report"; + if (matches(value, ["how do i", "how can i", "where do i", "what should", "setup", "set up", "configure", "verify", "dns", "domain", "docs"])) return "product_question"; + return "unknown"; +} + +function confidenceFor(label, signals, missingContext) { + if (missingContext.length > 0) return 0.25; + if (label === "unknown") return 0.35; + if (label === "unsafe_send_request") return 0.94; + if (signals.length >= 3) return 0.88; + if (signals.length === 2) return 0.78; + return 0.66; +} + +function urgencyFor(label, value) { + if (label === "abuse" || label === "account_access") return "high"; + if (label === "bug_report" && matches(value, ["production", "all users", "down", "data loss", "security"])) return "critical"; + if (label === "billing" || label === "bug_report" || label === "unsafe_send_request") return "elevated"; + return "normal"; +} + +function queueFor(label, policy, missingContext) { + const queues = objectOrNull(policy.queues) ?? {}; + if (missingContext.length > 0) { + return { + name: stringValue(queues.manual_review) ?? "support.manual_review", + priority: "elevated", + reason: "The inbox packet is missing required bounded context.", + }; + } + const mapping = { + product_question: ["reply_drafts", "support.reply_drafts", "normal", "Safe product question with enough context for a draft."], + bug_report: ["engineering_intake", "support.engineering_intake", "elevated", "Bug reports need reproduction-focused engineering triage."], + billing: ["billing_review", "support.billing_review", "elevated", "Billing requests require verified account context."], + account_access: ["account_review", "support.account_review", "high", "Account access requests require identity and ownership verification."], + abuse: ["abuse_review", "support.abuse_review", "high", "Abuse reports require specialist review."], + unsafe_send_request: ["manual_review", "support.manual_review", "high", "The message requested bypassing send approval."], + unknown: ["manual_review", "support.manual_review", "elevated", "The message does not contain enough signal for a safe draft."], + }; + const [key, fallback, priority, reason] = mapping[label] ?? mapping.unknown; + return { name: stringValue(queues[key]) ?? fallback, priority, reason }; +} + +function buildDraftReply({ subject, body, senderEmail, senderName, productName, supportSignature }) { + const greeting = senderName ? `Hi ${firstName(senderName)},` : "Hi,"; + const response = answerForProductQuestion(`${subject ?? ""}\n${body ?? ""}`, productName); + return { + proposed: true, + to: senderEmail, + subject: subject && /^re:/i.test(subject) ? subject : `Re: ${subject ?? "your question"}`, + body: [ + greeting, + "", + response, + "", + "Before this is sent, an operator should confirm the thread context and approve the send-as handoff. This skill has not sent the message.", + "", + "Thanks,", + supportSignature, + ].join("\n"), + }; +} + +function answerForProductQuestion(value, productName) { + const normalized = normalize(value); + if (matches(normalized, ["dns", "domain", "dkim", "spf", "dmarc", "verify"])) { + return `For ${productName} domain verification, compare the host/name and value fields with the records shown in setup, then wait for DNS propagation and run the verification check again. If your DNS provider appends the root domain automatically, make sure the host value is not duplicated.`; + } + if (matches(normalized, ["webhook", "api", "integration"])) { + return `For ${productName} integrations, confirm the endpoint or API key is scoped to the environment you are testing, retry one minimal request, and save the response body if it still fails.`; + } + return `For ${productName}, follow the documented setup step named in your message and retry once the required fields are complete. If it still fails, reply with the exact error text, timestamp, and the screen where it happened.`; +} + +function draftBlocker(label, missingContext, unsafeSend) { + if (missingContext.length > 0) return `Missing required context: ${missingContext.join(", ")}.`; + if (unsafeSend) return "The request asked to bypass send approval."; + if (["billing", "account_access", "abuse"].includes(label)) return "This category requires private-state or specialist review before drafting."; + if (label === "bug_report") return "Bug reports should be routed with reproduction evidence before a customer-facing reply is drafted."; + return "The message is too ambiguous for a safe reply draft."; +} + +function rationaleFor(label, missingContext, unsafeSend) { + if (missingContext.length > 0) return "Required bounded inbox fields are missing."; + if (unsafeSend) return "The message contains send-without-approval language."; + const rationales = { + product_question: "The message asks a bounded setup or product-use question.", + bug_report: "The message reports failure or broken behavior.", + billing: "The message references payment, invoices, plans, or refunds.", + account_access: "The message references login, password, ownership, or account access.", + abuse: "The message references abuse, spam, phishing, fraud, or compromise.", + unknown: "The message lacks enough known signals for a confident route.", + }; + return rationales[label] ?? rationales.unknown; +} + +function missingContextFor({ source, threadId, latest, latestBody, senderEmail }) { + const missing = []; + if (!source) missing.push("source"); + if (!threadId) missing.push("thread_id"); + if (!latest) { + missing.push("latest message"); + return missing; + } + if (!senderEmail) missing.push("sender email"); + if (!latestBody) missing.push("message body"); + return missing; +} + +function signalsFor(label, value) { + const dictionaries = { + product_question: ["how do i", "how can i", "where do i", "what should", "setup", "set up", "configure", "verify", "dns", "domain", "docs"], + bug_report: ["error", "bug", "broken", "500", "failed", "crash", "exception", "does not work", "regression"], + billing: ["invoice", "billing", "charge", "refund", "paid", "payment", "subscription", "plan", "tax"], + account_access: ["login", "password", "reset", "locked out", "2fa", "mfa", "owner", "access", "account"], + abuse: ["abuse", "spam", "phishing", "harassment", "threat", "fraud", "compromised"], + unsafe_send_request: ["send this now", "send without approval", "bypass approval", "skip approval", "auto-send", "autosend"], + unknown: [], + }; + return (dictionaries[label] ?? []).filter((signal) => value.includes(signal)); +} + +function matches(value, needles) { + return needles.some((needle) => value.includes(needle)); +} + +function normalize(value) { + return String(value ?? "").toLowerCase().replace(/\s+/g, " ").trim(); +} + +function firstName(value) { + return String(value ?? "").split(/\s+/)[0]?.replace(/[^a-zA-Z'-]/g, "") || null; +} + +function stringValue(value) { + return typeof value === "string" && value.trim().length > 0 ? value.trim() : null; +} + +function objectValue(value, name) { + if (!value || typeof value !== "object" || Array.isArray(value)) { + fail(`${name} must be an object`); + } + return value; +} + +function objectOrNull(value) { + return value && typeof value === "object" && !Array.isArray(value) ? value : null; +} + +function fail(message) { + process.stderr.write(`${message}\n`); + process.exit(64); +}