diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml new file mode 100644 index 00000000..ab474452 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -0,0 +1,57 @@ +# SPDX-License-Identifier: MPL-2.0 +name: "Bug report" +description: "Report something that is broken or behaving unexpectedly." +labels: ["bug"] +body: + - type: textarea + id: what-happened + attributes: + label: "What happened?" + description: "Describe the problem. What did you see?" + validations: + required: true + - type: textarea + id: repro-steps + attributes: + label: "Steps to reproduce" + description: "Exact steps that trigger the problem, one per line." + placeholder: "1. ...\n2. ...\n3. ..." + validations: + required: true + - type: textarea + id: expected + attributes: + label: "Expected behaviour" + description: "What did you expect to happen instead?" + - type: input + id: version + attributes: + label: "Version" + placeholder: "e.g. 0.4.7 / commit SHA" + validations: + required: true + - type: dropdown + id: component + attributes: + label: "Component" + description: "Which part of boj-server is affected?" + options: + - "MCP bridge" + - "Elixir REST core" + - "Cartridges" + - "ABI / proofs" + - "Coordination (coord_*)" + - "Tray / TUI" + - "CI / workflows" + - "Other" + - type: textarea + id: logs + attributes: + label: "Relevant logs / stack trace" + description: "Paste any logs or stack traces." + render: shell + - type: textarea + id: environment + attributes: + label: "Environment" + description: "OS, runtime (Deno/Node/Bun) and versions, deployment shape (stdio/http/container)" diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..86cc18a6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,2 @@ +# SPDX-License-Identifier: MPL-2.0 +blank_issues_enabled: true diff --git a/.machine_readable/integrations/feedback-o-tron.a2ml b/.machine_readable/integrations/feedback-o-tron.a2ml index 5381604e..6841c69b 100644 --- a/.machine_readable/integrations/feedback-o-tron.a2ml +++ b/.machine_readable/integrations/feedback-o-tron.a2ml @@ -5,9 +5,17 @@ name = "feedback-o-tron" type = "bug-reporter" repository = "https://github.com/hyperpolymath/feedback-o-tron" +cartridge = "bug-filing-mcp" + +[intake] +transport = "http" +endpoint = "http://127.0.0.1:7722" +enable-with = "FEEDBACK_A_TRON_HTTP=1" +tools = ["research_feedback", "synthesize_feedback", "submit_feedback"] [reporting-config] platforms = ["github", "gitlab", "bugzilla"] deduplication = true audit-logging = true +template-validation = "fail-closed" auto-file-upstream = "on-external-dependency-failure" diff --git a/cartridges/bug-filing-mcp/README.adoc b/cartridges/bug-filing-mcp/README.adoc new file mode 100644 index 00000000..2c16d921 --- /dev/null +++ b/cartridges/bug-filing-mcp/README.adoc @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Copyright (c) 2026 Jonathan D.A. Jewell += bug-filing-mcp + +Autonomous bug-report filing cartridge, v0.2. It is the boj-side packaging of the +real *feedback-o-tron* engine for the autonomous bug-reporting pipeline +(see `feedback-o-tron/docs/AUTONOMOUS-BUG-PIPELINE.adoc`). + +[IMPORTANT] +==== +*This is not `feedback-mcp`.* `feedback-mcp` is an unrelated in-memory *sentiment +counter*. Per owner decision *D0 = new wrapping cartridge*, that cartridge is left +untouched and this *new* cartridge wraps the real bug-filing engine. The two only +share the `127.0.0.1:7722` placeholder port; run one backend or the other. +==== + +== What it does + +v0.2 exposes the engine's full interactive loop as three tools: + +. `research_feedback` — *before filing anything*: searches the target forge for + similar existing issues (scored by title similarity), checks the engine's + local dedup index (with recurrence stats, so a recurring theme is visible as + a pattern rather than a one-off), and returns the target repo's issue-form + questions. +. `synthesize_feedback` — shapes raw feedback into a report fit for the + receiver: classifies intent, fetches the repo's own issue template, hydrates + its fields from context and system state, and returns a draft plus the *open + questions* still needed. The caller (an LLM agent, typically) resolves those + with its user and iterates. +. `submit_feedback` — files to one or more forges (GitHub, GitLab, Bitbucket, + Codeberg, Bugzilla, email). When `template_data` is supplied, answers are + validated against the fetched form schema (*fail closed*) and rendered as the + issue body. Dedup and audit logging are engine-side. + +== The usefulness doctrine + +The intent gate is *usefulness, not tone*: + +* Harsh-but-substantive feedback keeps its signal — nothing is sanitized away. +* Zero-signal hostility is rejected with a stated reason (audit-logged + engine-side, never filed). A rejection is a *successful* tool result: + `{"rejected": true, "reason": ...}`. +* Mixed content is salvaged: the actionable core is extracted, the hostile + wrapping stripped, and both facts are reported back + (`salvaged: true`, `stripped_reason`). +* Useless-but-genuine feedback is never silently discarded — it comes back as + open questions that would make it fileable. + +== Core status (Teranga) + +This cartridge is Teranga-tier: bug filing is a core capability of the boj +system. It still requires the engine running locally (see below) — the manifest +does not claim `available: true`; availability is a deployment fact, not a +catalogue claim. + +== How it wires + +[source,text] +---- +boj gateway :7700 + POST /cartridge/bug-filing-mcp/invoke {tool, arguments} + -> bug-filing-mcp/mod.js (Deno JS worker) + -> POST http://127.0.0.1:7722/api/v1/{research,synthesize,submit}_feedback + -> feedback-o-tron Synthesis Engine + Submitter -> gh / glab / REST ... +---- + +The cartridge is a thin HTTP client; classification, template hydration, +validation, dedup, audit, rate limiting and dispatch all live in the engine. +The `ffi/` and `abi/` directories carry the standard cartridge layout: the +Idris2 module (`abi/BugFilingMcp/SafeBugFiling.idr`, compiled in CI-style +checks) proves the pipeline's state machine — a rejected report has *no path* +to submission, and submission is only reachable through validation. The Zig +FFI answers with a structured delegation notice (the live path is mod.js); +no `.so` is declared in the manifest. + +== Prerequisites + +Run the feedback-o-tron engine with its HTTP intake enabled: + +[source,sh] +---- +FEEDBACK_A_TRON_HTTP=1 feedback-a-tron # listens on 127.0.0.1:7722 +# override: FEEDBACK_A_TRON_HTTP_PORT, FEEDBACK_A_TRON_HTTP_BIND +---- + +Point the cartridge elsewhere with `BUG_FILING_BACKEND_URL` if needed. + +== The interactive loop, end to end + +[source,text] +---- +1. research_feedback {repo, title} -> similar issues + template questions +2. synthesize_feedback {raw_feedback, repo} -> draft + open_questions (or rejected/salvaged) +3. ... resolve open_questions with your user, repeat 2 if needed ... +4. submit_feedback {title, body, repo, template, template_data, dry_run:true} -> preview +5. submit_feedback {..., dry_run:false} -> filed, deduplicated, audit-logged +---- + +== Tools + +[cols="1,1,3",options="header"] +|=== +| Tool | Required | Meaning +| `research_feedback` | `repo`, `title` | Duplicate search + template questions; `limit` (≤20), `body`, `include_templates` optional +| `synthesize_feedback` | `raw_feedback`, `repo` | Intent-gated, template-hydrated draft + open questions; `context`, `system_state`, `template`, `network_probe` optional +| `submit_feedback` | `title`, `body`, `repo` | Multi-forge filing; `template`+`template_data` validated fail-closed; `platforms`, `labels`, `dry_run`, `skip_dedupe` optional +|=== + +If the backend is not running, the cartridge returns a `503` with a hint to +start the engine with `FEEDBACK_A_TRON_HTTP=1`. diff --git a/cartridges/bug-filing-mcp/abi/BugFilingMcp/SafeBugFiling.idr b/cartridges/bug-filing-mcp/abi/BugFilingMcp/SafeBugFiling.idr new file mode 100644 index 00000000..9b82450e --- /dev/null +++ b/cartridges/bug-filing-mcp/abi/BugFilingMcp/SafeBugFiling.idr @@ -0,0 +1,158 @@ +-- SPDX-License-Identifier: MPL-2.0 +-- Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) +||| BugFilingMcp.SafeBugFiling: Formally verified bug-filing pipeline states. +||| +||| Cartridge: bug-filing-mcp (wraps the feedback-o-tron engine) +||| Matrix cell: Development domain x {MCP, REST} protocols +||| +||| This module defines the interactive filing pipeline as a state machine +||| that prevents: +||| - Submitting a report that was rejected by the usefulness gate +||| - Submitting template answers that were never validated +||| - Skipping research when a duplicate check is required +||| +||| Doctrine encoded (feedback-o-tron design intent): +||| - the gate is usefulness, not tone: Salvaged is a live state, not an end +||| - zero-signal hostility dead-ends in Rejected (no path to Submitted) +||| - open questions loop Synthesized -> Synthesized until validation +||| +||| State machine: +||| Draft -> Researched -> Synthesized -> Validated -> Submitted +||| (Synthesized may be Salvaged first; Rejected is terminal) +module BugFilingMcp.SafeBugFiling + +import Data.List + +%default total + +-- ═══════════════════════════════════════════════════════════════════════════ +-- Filing Pipeline State Machine +-- ═══════════════════════════════════════════════════════════════════════════ + +||| Pipeline lifecycle states for one piece of feedback. +public export +data FilingState + = Draft -- raw feedback exists, nothing checked + | Researched -- duplicates + template questions fetched + | Salvaged -- hostile wrapping stripped; actionable core kept + | Synthesized -- template hydrated; open questions may remain + | Validated -- answers passed the form-schema boundary + | Submitted -- dispatched to the forge(s) + | Rejected -- zero-signal hostility: terminal, never filed + +||| Equality for filing states. +public export +Eq FilingState where + Draft == Draft = True + Researched == Researched = True + Salvaged == Salvaged = True + Synthesized == Synthesized = True + Validated == Validated = True + Submitted == Submitted = True + Rejected == Rejected = True + _ == _ = False + +||| Valid state transitions (enforced at the type level). +||| Critically there is NO constructor targeting Submitted except from +||| Validated, and NO constructor leaving Rejected. +public export +data ValidTransition : FilingState -> FilingState -> Type where + Research : ValidTransition Draft Researched + SalvageCore : ValidTransition Researched Salvaged + Synthesize : ValidTransition Researched Synthesized + Resynth : ValidTransition Synthesized Synthesized -- open-questions loop + FromSalvage : ValidTransition Salvaged Synthesized + Validate : ValidTransition Synthesized Validated + Submit : ValidTransition Validated Submitted + RejectR : ValidTransition Researched Rejected + RejectD : ValidTransition Draft Rejected + +||| Runtime transition validator (mirrors ValidTransition). +public export +canTransition : FilingState -> FilingState -> Bool +canTransition Draft Researched = True +canTransition Researched Salvaged = True +canTransition Researched Synthesized = True +canTransition Synthesized Synthesized = True +canTransition Salvaged Synthesized = True +canTransition Synthesized Validated = True +canTransition Validated Submitted = True +canTransition Researched Rejected = True +canTransition Draft Rejected = True +canTransition _ _ = False + +||| Rejected is terminal: no transition out of it is ever valid. +export +rejectedIsTerminal : (s : FilingState) -> ValidTransition Rejected s -> Void +rejectedIsTerminal _ Research impossible +rejectedIsTerminal _ SalvageCore impossible +rejectedIsTerminal _ Synthesize impossible +rejectedIsTerminal _ Resynth impossible +rejectedIsTerminal _ FromSalvage impossible +rejectedIsTerminal _ Validate impossible +rejectedIsTerminal _ Submit impossible +rejectedIsTerminal _ RejectR impossible +rejectedIsTerminal _ RejectD impossible + +||| Submission only ever follows validation: the sole transition into +||| Submitted starts at Validated. +export +submitRequiresValidation : (s : FilingState) -> ValidTransition s Submitted -> s = Validated +submitRequiresValidation Validated Submit = Refl + +-- ═══════════════════════════════════════════════════════════════════════════ +-- MCP Tool Definitions +-- ═══════════════════════════════════════════════════════════════════════════ + +||| MCP tools exposed by this cartridge. +public export +data McpTool + = ToolResearch -- research_feedback + | ToolSynthesize -- synthesize_feedback + | ToolSubmit -- submit_feedback + +||| MCP tool name (must match cartridge.json). +public export +toolName : McpTool -> String +toolName ToolResearch = "research_feedback" +toolName ToolSynthesize = "synthesize_feedback" +toolName ToolSubmit = "submit_feedback" + +||| The pipeline state each tool drives toward. +public export +toolTarget : McpTool -> FilingState +toolTarget ToolResearch = Researched +toolTarget ToolSynthesize = Synthesized +toolTarget ToolSubmit = Submitted + +-- ═══════════════════════════════════════════════════════════════════════════ +-- C-ABI Exports +-- ═══════════════════════════════════════════════════════════════════════════ + +||| Filing state to integer for the C ABI. +public export +filingStateToInt : FilingState -> Int +filingStateToInt Draft = 0 +filingStateToInt Researched = 1 +filingStateToInt Salvaged = 2 +filingStateToInt Synthesized = 3 +filingStateToInt Validated = 4 +filingStateToInt Submitted = 5 +filingStateToInt Rejected = 6 + +||| Integer to filing state (unknown collapses to Rejected: fail closed). +public export +intToFilingState : Int -> FilingState +intToFilingState 0 = Draft +intToFilingState 1 = Researched +intToFilingState 2 = Salvaged +intToFilingState 3 = Synthesized +intToFilingState 4 = Validated +intToFilingState 5 = Submitted +intToFilingState _ = Rejected + +||| FFI: Validate a state transition. +export +bf_can_transition : Int -> Int -> Int +bf_can_transition from to = + if canTransition (intToFilingState from) (intToFilingState to) then 1 else 0 diff --git a/cartridges/bug-filing-mcp/abi/bug-filing-mcp.ipkg b/cartridges/bug-filing-mcp/abi/bug-filing-mcp.ipkg new file mode 100644 index 00000000..db7522a4 --- /dev/null +++ b/cartridges/bug-filing-mcp/abi/bug-filing-mcp.ipkg @@ -0,0 +1,12 @@ +-- SPDX-License-Identifier: MPL-2.0 +-- Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) +-- Note: Idris2 package identifiers reject hyphens, hence "bugfilingmcp". +package bugfilingmcp + +authors = "Jonathan D.A. Jewell" +version = 0.2.0 +sourcedir = "." + +modules = BugFilingMcp.SafeBugFiling + +depends = base diff --git a/cartridges/bug-filing-mcp/cartridge.json b/cartridges/bug-filing-mcp/cartridge.json new file mode 100644 index 00000000..d4552b7e --- /dev/null +++ b/cartridges/bug-filing-mcp/cartridge.json @@ -0,0 +1,141 @@ +{ + "$schema": "https://hyperpolymath.dev/standards/cartridges/cartridge-v1.json", + "spdx": "MPL-2.0", + "copyright": "Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath)", + "name": "bug-filing-mcp", + "version": "0.2.0", + "description": "Autonomous bug-report filing cartridge -- wraps the real feedback-o-tron engine (multi-forge issue submission with deduplication and audit logging) via its localhost HTTP intake. v0.2 exposes the engine's full interactive loop: research_feedback (search existing issues + the target repo's template questions, so duplicates are avoided), synthesize_feedback (intent classification with a usefulness-first gate, template hydration, open questions back to the caller), and submit_feedback (now accepting validated template_data). This is the boj-side packaging of the engine for the autonomous bug-reporting pipeline (feedback-o-tron/docs/AUTONOMOUS-BUG-PIPELINE.adoc, D0 = new wrapping cartridge). NOT the same as feedback-mcp, which is an unrelated in-memory sentiment counter.", + "domain": "development", + "category": "domain", + "tier": "Teranga", + "protocols": [ + "MCP", + "REST" + ], + "auth": { + "method": "none", + "env_var": null, + "credential_source": null + }, + "api": { + "base_url": "http://127.0.0.1:7722", + "content_type": "application/json" + }, + "tools": [ + { + "name": "research_feedback", + "description": "Research existing feedback before filing: searches the target forge for similar issues (scored by title similarity), checks the engine's local dedup index (with recurrence stats), and returns the repo's issue-form questions -- so duplicates are avoided and refinement is interactive. Call this first.", + "inputSchema": { + "type": "object", + "properties": { + "repo": { + "type": "string", + "description": "Target repository (owner/repo)" + }, + "title": { + "type": "string", + "description": "Working title / one-line summary of the feedback" + }, + "body": { + "type": "string", + "description": "Optional draft body to improve similarity matching" + }, + "limit": { + "type": "integer", + "description": "Max forge matches to return (default 5, max 20)" + }, + "include_templates": { + "type": "boolean", + "description": "Also fetch the repo's issue-form templates (default true)" + } + }, + "required": ["repo", "title"] + } + }, + { + "name": "synthesize_feedback", + "description": "Shape raw feedback into a report fit for the receiver: classifies intent (usefulness-first -- zero-signal abuse is rejected with a stated reason; mixed content has its actionable core salvaged and the stripping reported), fetches the target repo's issue-form template, hydrates its fields from context and system state, and returns a draft plus the open questions still needed. Resolve the open questions with your user, then call submit_feedback with the completed template_data.", + "inputSchema": { + "type": "object", + "properties": { + "raw_feedback": { + "type": "string", + "description": "The raw feedback/crash text to shape into a report" + }, + "repo": { + "type": "string", + "description": "Target repository (owner/repo)" + }, + "context": { + "type": "object", + "description": "Caller-provided context: title, version, expected, trajectory, logs" + }, + "system_state": { + "type": "object", + "description": "Caller-provided environment facts (OS, runtime versions)" + }, + "template": { + "type": "string", + "description": "Force a specific template file (e.g. bug.yml)" + }, + "network_probe": { + "type": "boolean", + "description": "Run NetworkVerifier preflight when the report looks network-related (default false)" + } + }, + "required": ["raw_feedback", "repo"] + } + }, + { + "name": "submit_feedback", + "description": "File a bug report / feedback / proposal to one or more forges (GitHub, GitLab, Bitbucket, Codeberg, Bugzilla, email) via the feedback-o-tron engine. Includes deduplication against existing issues and audit logging. When template_data is supplied the answers are validated against the target repo's issue-form schema (fail closed) and rendered as the issue body. Use dry_run to preview without filing.", + "inputSchema": { + "type": "object", + "properties": { + "title": { + "type": "string", + "description": "Issue/feedback title" + }, + "body": { + "type": "string", + "description": "Issue/feedback body (markdown supported)" + }, + "repo": { + "type": "string", + "description": "Target repository (owner/repo format)" + }, + "platforms": { + "type": "array", + "items": { + "type": "string", + "enum": ["github", "gitlab", "bitbucket", "codeberg", "bugzilla", "email"] + }, + "description": "Platforms to submit to (default: github)" + }, + "labels": { + "type": "array", + "items": { "type": "string" }, + "description": "Labels to apply (platform-dependent)" + }, + "template": { + "type": "string", + "description": "Issue-form template file this submission answers (e.g. bug.yml)" + }, + "template_data": { + "type": "object", + "description": "Map of template field id -> answer; validated against the fetched form schema and rendered as the issue body" + }, + "dry_run": { + "type": "boolean", + "description": "If true, show what would be submitted without actually submitting" + }, + "skip_dedupe": { + "type": "boolean", + "description": "If true, skip duplicate checking" + } + }, + "required": ["title", "body", "repo"] + } + } + ] +} diff --git a/cartridges/bug-filing-mcp/ffi/bug_filing_mcp_ffi.zig b/cartridges/bug-filing-mcp/ffi/bug_filing_mcp_ffi.zig new file mode 100644 index 00000000..4c3fded3 --- /dev/null +++ b/cartridges/bug-filing-mcp/ffi/bug_filing_mcp_ffi.zig @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) +// +// bug-filing-mcp FFI — ADR-0006 five-symbol cartridge ABI. +// +// This cartridge is a thin HTTP client over the feedback-o-tron engine's +// localhost intake (mod.js is the live dispatch path). The FFI layer is +// standard-layout scaffolding: invoke answers every declared tool with a +// structured delegation notice pointing at the HTTP backend, because a +// network round-trip does not belong in a synchronous C-ABI call. It is +// NOT the real engine and no .so is declared in cartridge.json. + +const std = @import("std"); + +const shim = @import("cartridge_shim.zig"); + +const CARTRIDGE_NAME_PTR: [*:0]const u8 = "bug-filing-mcp"; +const CARTRIDGE_VERSION_PTR: [*:0]const u8 = "0.2.0"; + +const DELEGATION_BODY = + "{\"result\":{\"status\":\"delegated\",\"transport\":\"http\"," ++ + "\"backend\":\"http://127.0.0.1:7722\"," ++ + "\"hint\":\"invoke via mod.js (Deno worker); start the engine with FEEDBACK_A_TRON_HTTP=1\"}}"; + +export fn boj_cartridge_init() callconv(.c) c_int { + return 0; +} + +export fn boj_cartridge_deinit() callconv(.c) void {} + +export fn boj_cartridge_name() callconv(.c) [*:0]const u8 { + return CARTRIDGE_NAME_PTR; +} + +export fn boj_cartridge_version() callconv(.c) [*:0]const u8 { + return CARTRIDGE_VERSION_PTR; +} + +export fn boj_cartridge_invoke( + tool_name: [*c]const u8, + json_args: [*c]const u8, + out_buf: [*c]u8, + in_out_len: [*c]usize, +) callconv(.c) i32 { + _ = json_args; + if (shim.invokeArgsNull(tool_name, out_buf, in_out_len)) return shim.RC_BAD_ARGS; + + const known = shim.toolIs(tool_name, "research_feedback") or + shim.toolIs(tool_name, "synthesize_feedback") or + shim.toolIs(tool_name, "submit_feedback"); + if (!known) return shim.RC_UNKNOWN_TOOL; + + return shim.writeResult(out_buf, in_out_len, DELEGATION_BODY); +} + +// ── Tests ── + +test "boj_cartridge_name returns bug-filing-mcp" { + const n = std.mem.span(boj_cartridge_name()); + try std.testing.expectEqualStrings("bug-filing-mcp", n); +} + +test "boj_cartridge_init returns 0" { + try std.testing.expectEqual(@as(c_int, 0), boj_cartridge_init()); +} + +test "invoke: each declared tool answers with a delegation notice" { + var buf: [512]u8 = undefined; + const tools = [_][]const u8{ + "research_feedback", + "synthesize_feedback", + "submit_feedback", + }; + for (tools) |t| { + var len: usize = buf.len; + const rc = boj_cartridge_invoke(t.ptr, "{}", &buf, &len); + try std.testing.expectEqual(@as(i32, 0), rc); + try std.testing.expect(std.mem.indexOf(u8, buf[0..len], "delegated") != null); + } +} + +test "invoke: unknown tool returns RC_UNKNOWN_TOOL" { + var buf: [64]u8 = undefined; + var len: usize = buf.len; + const rc = boj_cartridge_invoke("nope", "{}", &buf, &len); + try std.testing.expectEqual(shim.RC_UNKNOWN_TOOL, rc); +} + +test "invoke: buffer too small reports needed length" { + var buf: [4]u8 = undefined; + var len: usize = buf.len; + const rc = boj_cartridge_invoke("submit_feedback", "{}", &buf, &len); + try std.testing.expectEqual(shim.RC_BUFFER_TOO_SMALL, rc); + try std.testing.expect(len > 4); +} diff --git a/cartridges/bug-filing-mcp/ffi/build.zig b/cartridges/bug-filing-mcp/ffi/build.zig new file mode 100644 index 00000000..ec458ca4 --- /dev/null +++ b/cartridges/bug-filing-mcp/ffi/build.zig @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) + +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + // Shared ADR-0006 invoke-shim module (canonical copy alongside this file). + const shim_mod = b.addModule("cartridge_shim", .{ + .root_source_file = b.path("cartridge_shim.zig"), + .target = target, + .optimize = optimize, + }); + + const ffi_mod = b.addModule("bug_filing_mcp", .{ + .root_source_file = b.path("bug_filing_mcp_ffi.zig"), + .target = target, + .optimize = optimize, + }); + + ffi_mod.addImport("cartridge_shim", shim_mod); + + const lib = b.addLibrary(.{ + .name = "bug_filing_mcp", + .root_module = ffi_mod, + .linkage = .dynamic, + }); + lib.linkLibC(); + b.installArtifact(lib); + + const tests = b.addTest(.{ .root_module = ffi_mod }); + tests.linkLibC(); + const run_tests = b.addRunArtifact(tests); + const test_step = b.step("test", "Run FFI tests"); + test_step.dependOn(&run_tests.step); +} diff --git a/cartridges/bug-filing-mcp/ffi/cartridge_shim.zig b/cartridges/bug-filing-mcp/ffi/cartridge_shim.zig new file mode 100644 index 00000000..0e399dbb --- /dev/null +++ b/cartridges/bug-filing-mcp/ffi/cartridge_shim.zig @@ -0,0 +1,142 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) +// +// cartridge_shim.zig — Shared helpers for the ADR-0006 five-symbol +// cartridge ABI (`boj_cartridge_init / deinit / name / version / invoke`). +// +// The shim centralises the seven-code return convention, NUL-argument +// guards, tool-name comparison, and the buffer-too-small path so each +// cartridge's `boj_cartridge_invoke` can stay short — typically a tool +// table plus `shim.writeResult(...)`. +// +// Cartridges import this file by relative path (no build-graph change +// needed). Example: +// +// const shim = @import("../../../ffi/zig/src/cartridge_shim.zig"); +// +// export fn boj_cartridge_invoke( +// tool_name: [*c]const u8, +// json_args: [*c]const u8, +// out_buf: [*c]u8, +// in_out_len: [*c]usize, +// ) callconv(.c) i32 { +// _ = json_args; +// if (shim.invokeArgsNull(tool_name, out_buf, in_out_len)) return shim.RC_BAD_ARGS; +// const body = if (shim.toolIs(tool_name, "foo")) "{\"result\":{}}" +// else return shim.RC_UNKNOWN_TOOL; +// return shim.writeResult(out_buf, in_out_len, body); +// } + +const std = @import("std"); + +// ── Return codes (ADR-0006 §Return codes) ──────────────────────────── +// +// Frozen by ADR-0006. New failure modes compose these via the error +// JSON body — the integer surface does not grow without a follow-up ADR. + +pub const RC_SUCCESS: i32 = 0; +pub const RC_UNKNOWN_TOOL: i32 = -1; +pub const RC_BAD_ARGS: i32 = -2; +pub const RC_BUFFER_TOO_SMALL: i32 = -3; +pub const RC_RUNTIME_ERROR: i32 = -4; +pub const RC_PANIC: i32 = -5; +pub const RC_AUTH_DENIED: i32 = -6; + +// ── Invoke-path helpers ────────────────────────────────────────────── + +/// True if any of the three mandatory `boj_cartridge_invoke` output-path +/// pointers is null. Use at the top of every invoke to short-circuit to +/// `RC_BAD_ARGS`. +pub fn invokeArgsNull( + tool_name: [*c]const u8, + out_buf: [*c]u8, + in_out_len: [*c]usize, +) bool { + return tool_name == null or out_buf == null or in_out_len == null; +} + +/// Compare a C-NUL-terminated tool-name pointer against a Zig string +/// literal. Caller must have already verified `tool_name` is non-null +/// (usually via `invokeArgsNull`). +pub fn toolIs(tool_name: [*c]const u8, expected: []const u8) bool { + const s = std.mem.span(@as([*:0]const u8, @ptrCast(tool_name))); + return std.mem.eql(u8, s, expected); +} + +/// Copy `body` into `out_buf[0..*in_out_len]` (as a capacity) and update +/// `*in_out_len` to the number of bytes written. Returns `RC_SUCCESS`. +/// +/// If `body.len` exceeds the current capacity stored in `*in_out_len`, +/// sets `*in_out_len` to the required size and returns +/// `RC_BUFFER_TOO_SMALL` — the caller is then expected to re-allocate +/// and retry, per ADR-0006 §Memory ownership. +/// +/// Caller must have already verified that `out_buf` and `in_out_len` +/// are non-null. +pub fn writeResult( + out_buf: [*c]u8, + in_out_len: [*c]usize, + body: []const u8, +) i32 { + const cap = in_out_len.*; + if (body.len > cap) { + in_out_len.* = body.len; + return RC_BUFFER_TOO_SMALL; + } + @memcpy(out_buf[0..body.len], body); + in_out_len.* = body.len; + return RC_SUCCESS; +} + +// ── Tests ──────────────────────────────────────────────────────────── + +test "writeResult: body fits, writes and sets length" { + var buf: [64]u8 = undefined; + var len: usize = buf.len; + const rc = writeResult(&buf, &len, "hello"); + try std.testing.expectEqual(RC_SUCCESS, rc); + try std.testing.expectEqual(@as(usize, 5), len); + try std.testing.expectEqualStrings("hello", buf[0..len]); +} + +test "writeResult: too small returns -3 and sets required length" { + var buf: [2]u8 = undefined; + var len: usize = buf.len; + const rc = writeResult(&buf, &len, "hello"); + try std.testing.expectEqual(RC_BUFFER_TOO_SMALL, rc); + try std.testing.expectEqual(@as(usize, 5), len); +} + +test "writeResult: exact-fit succeeds" { + var buf: [5]u8 = undefined; + var len: usize = buf.len; + const rc = writeResult(&buf, &len, "hello"); + try std.testing.expectEqual(RC_SUCCESS, rc); + try std.testing.expectEqual(@as(usize, 5), len); +} + +test "writeResult: empty body" { + var buf: [4]u8 = undefined; + var len: usize = buf.len; + const rc = writeResult(&buf, &len, ""); + try std.testing.expectEqual(RC_SUCCESS, rc); + try std.testing.expectEqual(@as(usize, 0), len); +} + +test "toolIs: matches and rejects" { + const name: [*:0]const u8 = "foo"; + try std.testing.expect(toolIs(@ptrCast(name), "foo")); + try std.testing.expect(!toolIs(@ptrCast(name), "bar")); + try std.testing.expect(!toolIs(@ptrCast(name), "foobar")); + try std.testing.expect(!toolIs(@ptrCast(name), "fo")); +} + +test "invokeArgsNull: detects each null slot" { + var buf: [4]u8 = undefined; + var len: usize = 4; + const name: [*:0]const u8 = "x"; + try std.testing.expect(!invokeArgsNull(@ptrCast(name), &buf, &len)); + try std.testing.expect(invokeArgsNull(null, &buf, &len)); + try std.testing.expect(invokeArgsNull(@ptrCast(name), null, &len)); + try std.testing.expect(invokeArgsNull(@ptrCast(name), &buf, null)); +} diff --git a/cartridges/bug-filing-mcp/mod.js b/cartridges/bug-filing-mcp/mod.js new file mode 100644 index 00000000..7546e261 --- /dev/null +++ b/cartridges/bug-filing-mcp/mod.js @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) +// +// bug-filing-mcp/mod.js — autonomous bug-report filing. +// +// Wraps the real feedback-o-tron engine via its localhost HTTP intake +// (http://127.0.0.1:7722; run the engine with FEEDBACK_A_TRON_HTTP=1). +// Override the backend with BUG_FILING_BACKEND_URL. +// +// v0.2 tools (the interactive loop): research_feedback (avoid duplicates, +// get the repo's template questions) -> synthesize_feedback (intent-gated, +// template-hydrated draft + open questions) -> submit_feedback (validated +// template_data, multi-forge dispatch). +// +// This is the boj-side packaging of the engine for the autonomous bug-reporting +// pipeline (feedback-o-tron/docs/AUTONOMOUS-BUG-PIPELINE.adoc, contract C4 dispatch +// under D0 = new wrapping cartridge). It is NOT feedback-mcp (an unrelated +// in-memory sentiment counter that happens to share the 7722 placeholder port). + +const BASE_URL = Deno.env.get("BUG_FILING_BACKEND_URL") ?? "http://127.0.0.1:7722"; +const TIMEOUT_MS = 30_000; + +async function post(path, payload) { + const ctrl = new AbortController(); + const t = setTimeout(() => ctrl.abort(), TIMEOUT_MS); + try { + const r = await fetch(`${BASE_URL}${path}`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload ?? {}), + signal: ctrl.signal, + }); + const data = await r.json().catch(() => ({ error: "non-JSON response from bug-filing backend" })); + return { status: r.status, data }; + } catch (e) { + if (e.name === "AbortError") { + return { status: 504, data: { error: "bug-filing backend (feedback-o-tron) timed out" } }; + } + return { + status: 503, + data: { + error: `bug-filing backend unavailable: ${e.message}. ` + + `Start feedback-o-tron with FEEDBACK_A_TRON_HTTP=1 (listening on ${BASE_URL}).`, + }, + }; + } finally { + clearTimeout(t); + } +} + +export async function handleTool(toolName, args) { + switch (toolName) { + case "research_feedback": + return post("/api/v1/research_feedback", args ?? {}); + case "synthesize_feedback": + return post("/api/v1/synthesize_feedback", args ?? {}); + case "submit_feedback": + return post("/api/v1/submit_feedback", args ?? {}); + default: + return { status: 404, data: { error: `Unknown tool: ${toolName}` } }; + } +} diff --git a/cartridges/bug-filing-mcp/panels/manifest.json b/cartridges/bug-filing-mcp/panels/manifest.json new file mode 100644 index 00000000..0a97d1e0 --- /dev/null +++ b/cartridges/bug-filing-mcp/panels/manifest.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://panll.dev/schemas/panel-manifest/v1.json", + "spdx": "MPL-2.0", + "cartridge": "bug-filing-mcp", + "domain": "development", + "version": "0.2.0", + "panels": [ + { + "id": "bug-filing-engine-status", + "title": "Filing Engine", + "description": "Health of the local feedback-o-tron engine intake (127.0.0.1:7722) and counts of recent research/synthesize/submit calls.", + "type": "status-indicator", + "entrypoint": "panels/bug-filing-engine-status.js", + "size": { "cols": 2, "rows": 1 }, + "refresh_interval_ms": 5000, + "data_sources": [ + { "op": "Health", "interval_ms": 15000 }, + { "op": "SubmitFeedback", "on": "event" } + ] + } + ] +} diff --git a/cartridges/bug-filing-mcp/tests/integration_test.sh b/cartridges/bug-filing-mcp/tests/integration_test.sh new file mode 100755 index 00000000..08d46add --- /dev/null +++ b/cartridges/bug-filing-mcp/tests/integration_test.sh @@ -0,0 +1,82 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MPL-2.0 +# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) +# +# Integration test for bug-filing-mcp cartridge. +# This cartridge is a thin HTTP client over the feedback-o-tron engine's +# localhost intake, so the wire it owns is tested directly against the +# backend. Skips (exit 0) when the engine is not running. + +set -euo pipefail + +BACKEND_URL="${BUG_FILING_BACKEND_URL:-http://127.0.0.1:7722}" +PASS=0 +FAIL=0 + +if ! curl -sf "${BACKEND_URL}/health" > /dev/null 2>&1; then + echo "SKIP (engine not running — start feedback-o-tron with FEEDBACK_A_TRON_HTTP=1)" + exit 0 +fi + +check() { + local desc="$1" path="$2" payload="$3" expect="$4" + local response + response=$(curl -s -X POST "${BACKEND_URL}${path}" \ + -H "Content-Type: application/json" \ + -d "${payload}" 2>/dev/null || echo '{"error":"connection_failed"}') + + if echo "$response" | grep -q "$expect"; then + echo "PASS: $desc" + PASS=$((PASS + 1)) + else + echo "FAIL: $desc (expected '$expect' in response)" + echo " Got: $response" + FAIL=$((FAIL + 1)) + fi +} + +echo "=== bug-filing-mcp integration tests (backend: ${BACKEND_URL}) ===" +echo "" + +health_response=$(curl -s "${BACKEND_URL}/health" 2>/dev/null || echo '{"error":"connection_failed"}') +if echo "$health_response" | grep -q '"status":"ok"'; then + echo "PASS: health reports ok" + PASS=$((PASS + 1)) +else + echo "FAIL: health reports ok" + echo " Got: $health_response" + FAIL=$((FAIL + 1)) +fi + +check "submit_feedback: dry_run previews without filing" \ + "/api/v1/submit_feedback" \ + '{"title":"integration test","body":"dry run only","repo":"example/example","dry_run":true}' \ + "dry_run" + +check "submit_feedback: missing fields rejected" \ + "/api/v1/submit_feedback" '{}' "missing_required_fields" + +check "research_feedback: missing fields rejected" \ + "/api/v1/research_feedback" '{}' "missing_required_fields" + +check "research_feedback: returns forge + local + templates sections" \ + "/api/v1/research_feedback" \ + '{"repo":"example/example","title":"integration probe"}' \ + '"local"' + +check "synthesize_feedback: missing fields rejected" \ + "/api/v1/synthesize_feedback" '{}' "missing_required_fields" + +check "synthesize_feedback: zero-signal hostility is rejected with a reason" \ + "/api/v1/synthesize_feedback" \ + '{"raw_feedback":"you all suck, worst tool ever","repo":"example/example"}' \ + '"rejected":true' + +check "synthesize_feedback: real bug text yields a draft" \ + "/api/v1/synthesize_feedback" \ + '{"raw_feedback":"crashes with ** (RuntimeError) boom when saving","repo":"example/example"}' \ + '"draft"' + +echo "" +echo "=== Results: ${PASS} passed, ${FAIL} failed ===" +exit $FAIL diff --git a/mcp-bridge/lib/offline-menu.js b/mcp-bridge/lib/offline-menu.js index bb6fab31..28bd0aca 100644 --- a/mcp-bridge/lib/offline-menu.js +++ b/mcp-bridge/lib/offline-menu.js @@ -39,6 +39,7 @@ export const OFFLINE_MENU = { { name: "ffmpeg-mcp", version: "0.1.0", domain: "Multi-modal", protocols: ["MCP","REST"], status: "Available", available: true, notes: "Local FFmpeg gateway — probe/transcode/extract/concat/trim; not Worker-compatible (local-only); routed via boj_multimodal" }, { name: "codeseeker-mcp", version: "0.1.0", domain: "Code Intelligence", protocols: ["MCP","REST"], status: "Available", available: true }, { name: "lang-mcp", version: "0.1.0", domain: "Languages", protocols: ["MCP","REST"], status: "Available", available: true }, + { name: "bug-filing-mcp", version: "0.2.0", domain: "Development", protocols: ["MCP","REST"], status: "Available", available: true, notes: "Autonomous bug filing via the real feedback-o-tron engine — research_feedback (duplicate search + template questions), synthesize_feedback (usefulness-gated draft + open questions), submit_feedback (validated template_data, multi-forge). Requires the engine running locally: FEEDBACK_A_TRON_HTTP=1 on 127.0.0.1:7722. NOT feedback-mcp (unrelated sentiment counter)." }, ], tier_shield: [ { name: "secrets-mcp", version: "0.1.0", domain: "Secrets", protocols: ["MCP","REST"], status: "Available", available: true }, @@ -47,9 +48,9 @@ export const OFFLINE_MENU = { tier_ayo: [ { name: "local-coord-mcp", version: "0.9.0", domain: "Agent", protocols: ["MCP","Agentic"], status: "Available", available: true, notes: "Localhost-only (127.0.0.1:7745) multi-instance AI coordination — peer discovery, typed envelopes, task claiming, master/journeyman/apprentice supervision with quarantine + watchdog TTL + track-record affinity + capability advertisement" }, ], - // `total` reflects the full cartridges/ directory (125 cartridges on disk); + // `total` reflects the full cartridges/ directory (127 cartridges on disk); // the tier_* arrays above enumerate the named ones exposed through the // offline menu. Regenerate counts with // `node mcp-bridge/lib/generate-offline-menu.js`. - summary: { total: 125, ready: 24, mounted: 0 }, + summary: { total: 127, ready: 24, mounted: 0 }, };