Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"name": "loopback",
"source": "./plugin",
"description": "Feedback hub: pin feedback on your live app; agents claim, fix, and write back over MCP; the pin turns green.",
"version": "0.9.2"
"version": "0.9.3"
}
]
}
82 changes: 82 additions & 0 deletions .github/ISSUE_TEMPLATE/bug_report.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
name: Bug report
description: Something behaves differently from what the docs say it does.
labels: ["bug"]
body:
- type: markdown
attributes:
value: |
Every field below is something the first triage question would ask for
anyway. If the problem is exploitable, do not file it here — see
SECURITY.md for the private channel.

- type: textarea
id: what
attributes:
label: What happened, and what you expected instead
validations:
required: true

- type: textarea
id: repro
attributes:
label: Steps to reproduce
description: The smallest sequence that shows it, including the command you ran.
placeholder: |
1. loopback-mcp-server --http
2. ...
3. ...
validations:
required: true

- type: input
id: version
attributes:
label: Loopback version
description: >-
`loopback-mcp-server --version` prints it; a running hub
also reports it at `GET /health`. Or read `version` from package.json.
validations:
required: true

- type: input
id: node
attributes:
label: Node version
description: "`node -v` — Loopback needs >= 22.13 for built-in node:sqlite."
validations:
required: true

- type: dropdown
id: agent
attributes:
label: Agent / client
options:
- Claude Code
- Codex
- Gemini CLI
- Another MCP client
- No agent — widget or HTTP only
validations:
required: true

- type: dropdown
id: transport
attributes:
label: Transport
description: How the server was running when it happened.
options:
- stdio (the agent spawns the server)
- --http on 127.0.0.1 (the hub)
- --http --host <non-loopback> (LAN bind, bearer token)
- Not applicable
validations:
required: true

- type: textarea
id: evidence
attributes:
label: Server output, failing request, console trail
description: >-
Server stderr, the browser console, the response body of a failing call.
Redact any token before pasting.
render: text
11 changes: 11 additions & 0 deletions .github/ISSUE_TEMPLATE/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Blank issues stay enabled on purpose: this project needs adopters more than it
# needs tidy metadata, and a stranger with a question should not be turned away
# at the door. The two forms are the guided path, not a toll gate.
blank_issues_enabled: true
contact_links:
- name: Docs, quickstart and the surface matrix
url: https://github.com/joshidikshant/loopback#readme
about: Setup for Claude Code / Codex / Gemini CLI, the HTTP surface, and where the widget does and does not work.
- name: Report a security vulnerability
url: https://github.com/joshidikshant/loopback/security/advisories/new
about: Anything exploitable goes here privately, never in a public issue.
44 changes: 44 additions & 0 deletions .github/ISSUE_TEMPLATE/feature_request.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: Feature request
description: Propose something Loopback should do that it does not.
labels: ["enhancement"]
body:
- type: markdown
attributes:
value: |
Loopback is deliberately only the bus and the capture layer — crash
capture, session replay and browser driving are borrowed from mature
MCP-native tools rather than rebuilt (docs/02-build-vs-borrow-memo.md).
A proposal that moves that line needs to say why borrowing does not work.

- type: textarea
id: problem
attributes:
label: The problem
description: What you hit in real use — not the feature, the thing that made you want it.
validations:
required: true

- type: textarea
id: proposal
attributes:
label: What you want it to do
validations:
required: true

- type: textarea
id: alternatives
attributes:
label: What you tried instead
description: >-
The existing doors are the widget, `POST /ingest`, the MCP tools and the
/queue page. Which came closest, and where did it stop?

- type: checkboxes
id: evidence
attributes:
label: Evidence
description: >-
Deferred-until-demanded is the default here — the roadmap holds several
features waiting for exactly this.
options:
- label: I have hit this in real use, not just in principle
19 changes: 19 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
## What this changes

<!-- One or two lines. Link the issue, or the queue item id if it came in that way. -->

## Why

<!-- What broke, or what was missing. Evidence beats description. -->

## Checks

- [ ] `npm run build` and `npm run smoke` pass locally
- [ ] the gates this change touches pass — the list is in CONTRIBUTING.md
- [ ] a new gate comes with its canary case in `scripts/canary-all.mjs`, so it is
proven to fail when its subject breaks
- [ ] a new number in the docs comes with a check in `scripts/docs-facts-gate.mjs`
- [ ] no generated file was hand-edited — playbook changes go into
`integrations/instructions-src.md` and `skills/loopback/SKILL.md` first

Delete a line that does not apply rather than leaving it unticked.
7 changes: 7 additions & 0 deletions .github/workflows/canary.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,11 @@ jobs:
- run: npm ci
- run: npm run build
- run: npx playwright install --with-deps chromium
# The release-preflight cases read a commit's CI conclusion through `gh`,
# which needs a token. github.token is the run's own credential, not event
# data, and it is passed via env rather than interpolated into a command.
# Without it the gate fails closed and its red case would pass for the
# wrong reason — which is exactly what its green case catches.
- run: node scripts/canary-all.mjs
env:
GH_TOKEN: ${{ github.token }}
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ jobs:
# Design anti-patterns in the shipped UI. Runs after dashboard-gate
# because it scans the built output, which that gate proves is current.
- run: node scripts/impeccable-gate.mjs
# Re-measures the numbers the docs assert (widget size, tool count, HTTP
# routes, registry items). Every other gate checks structure; the drift
# that actually accumulated was entirely in hand-typed figures.
- run: node scripts/docs-facts-gate.mjs

e2e:
runs-on: ubuntu-latest
Expand Down
11 changes: 8 additions & 3 deletions .github/workflows/readme-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,15 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
# This step used to invoke linkinator directly and scanned ZERO links
# while reporting success — extra path arguments are ignored, and the
# bare 127.0.0.1 skip matched linkinator's own local server root. The
# gate asserts a minimum link count per target so an empty crawl fails.
- name: Link check (README, docs, integrations)
run: >
npx --yes linkinator README.md docs integrations --markdown
--skip "127.0.0.1|localhost|^#|shields.io"
run: node scripts/link-gate.mjs
- name: Star-begging guard (growth-kit disqualifier class)
run: |
! grep -riE "star (us|this|the repo)|give (us|it) a star|drop a star|smash[^.]*star" README.md docs integrations
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ dist/
dashboard/src/tokens.generated.css
dashboard/node_modules/
.dashboard-freshness/
# tsc's incremental cache — machine-generated churn, and the only build artifact
# that was ever tracked. public/ build output IS committed, deliberately: it is
# how the shadcn registry and the dashboard are served.
*.tsbuildinfo

# Impeccable (design anti-pattern detector).
# The CI gate runs the version-pinned npm devDependency, so nothing here is
Expand Down
146 changes: 141 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,139 @@ All notable changes to Loopback are documented here. The format follows

## [Unreleased]

## [0.9.3] — accuracy pass: a red CI nobody was watching, and docs nothing checked

### Fixed — CI had been red for six days, and three releases shipped on top of it
`ci.yml` failed on **sixteen consecutive runs**, from 2026-07-27 (`f7dfa27`)
through 2026-08-02. Last green was `623aa05`. 0.9.0, 0.9.1 and 0.9.2 were all
published inside that window, so all three shipped two real WCAG 1.4.4/1.4.10
failures as product defects, not just a red badge.

The a11y gate had **never passed on CI at all**: it was added in `d2ca476` and
pushed in a batch with 27 other commits, so its first run was already red.

The bug was real and one line. `Updated` rendered `item.updated_at` as a bare
ISO timestamp with no `break-all`, while every other value in that card
(project, source/reporter, assignee, route) had one. An ISO string is a single
24-char token with no break opportunity, so at 200% text zoom it set a 236px
min-content floor that the section's `min-w-0` could not shrink. On macOS that
lands at 301px and passes a 320px viewport; on CI's Linux font metrics it
measures ~325px — exactly the `{"s":325,"c":320}` in every failing run, and why
it never reproduced locally. Now clears 320px with 40px of headroom.

### Added — `release-preflight`: publishing requires a green, pushed, tagged commit
The gates verify the source. `verify:release` deliberately runs *after* a
publish, against published artifacts, where npm's immutability means it can only
report damage. Nothing ever asked the question in between, which is how a red
`main` stayed publishable three times.

`release-preflight` runs as `prepublishOnly`: clean tree, commit pushed, a
completed successful `ci.yml` run for that exact sha, and a `v<version>` tag
pointing at it. It **fails closed** — an unreachable GitHub is a refusal, not a
pass, since "unknown" is the state that let the last three releases through.
`LOOPBACK_ALLOW_RED_CI=1` is the explicit, auditable override.

Two traps found by running it rather than reading it: the GitHub API matches
`head_sha` on the full 40-char sha only, so an abbreviated sha returns an empty
run list — indistinguishable from "never tested"; and a fail-closed gate sails
through a canary that only checks for a non-zero exit, so the sweep asserts
**both** directions against two immutable commits (`10a1c6d` red must fail,
`623aa05` green must pass).

Tags `v0.9.0`, `v0.9.1` and `v0.9.2` were created retroactively. Tag discipline
had stopped exactly when public publishing began: everything from v0.3.0 to
v0.8.0 is tagged, and the only three versions that ever reached npm were not.

### Added — `docs-facts-gate`: the numbers in the docs are re-derived from the code
Every structural gate was green while the docs drifted, because none of them
ever read a *claim*. Everything that had rotted was a hand-typed figure:

- the widget was quoted **four** ways — 46KB/15KB, 57KB/19KB, 57,183 B, ~29KB —
against a real 59,443 B, and two separate commits each claimed to have
"corrected the widget size" while fixing one occurrence out of two;
- "The MCP bus — 10 tools" sat above a 9-row table, missing
`loopback_update_feedback`, which the 0.8.0 entry below announced;
- the HTTP surface table omitted all three attachment endpoints;
- the shadcn section documented two registry items and shipped three;
- the repo map claimed `init` writes `.claude-plugin/` (it does not) and that
`init-gate` re-renders it (it asserts a version), and omitted `.github/` and
`.impeccable/`.

Measuring it exposed a smaller error inside the bigger one: "19,751 B gzipped on
the wire" was measured with the `gzip(1)` CLI, but the server serves `gzipSync`
at zlib's default level — 19,770 B. Three plausible numbers for one file, so the
gate measures it the way `src/http.ts` actually produces it.

The gate also caught a security claim CI never could: the README said "Three
endpoints stay open on a LAN bind" and listed three, while `requireAuth` lets
four through — `GET /health` was open in the code and absent from the table. It
now derives the open set from `requireAuth` itself.

And then the canary caught **this gate** being decorative, which is the whole
argument for having one. The first version substring-searched the table for each
open path, so the canary's mutation — renaming the row to `/health-REMOVED` —
still *contained* `/health` and the check passed with its subject broken. It now
parses the path out of each row's `METHOD /path` cell and compares sets in both
directions, so a renamed, missing or invented row all fail.

### Fixed — the README's shadcn section was mangled, on GitHub and on npm
A sentence cut off mid-clause, a contradictory replacement pasted over it, and
an orphan `free).` fragment. Live on `main` and in the published 0.9.2 readme,
which is why this needed a release rather than a docs commit.

### Fixed — `link-gate`: the link check had never checked a link
The CI step had been green since the day it was added while scanning **zero**
links, for two independent reasons: `linkinator README.md docs integrations`
silently ignores every path after the first, and `--skip "127.0.0.1|…"` matched
the root linkinator serves the files from, so the crawl never started. That one
pattern took the scan from 24 links to 0, and "Successfully scanned 0 links"
exits 0. The gate now asserts a **minimum link count per target**, because the
failure was never a broken link — it was an empty crawl.

### Added — `init-gate` covers the seam between the two canonical sources
Every parity check ran from one canonical source down to its renderings.
Nothing compared the two canonical sources to *each other*, and
`integrations/instructions-src.md` says outright that the skill body "mirrors
this text and must be updated with it" — a manual sync, and the last drift class
with no check. They are deliberately not byte-identical, so the invariant is the
loop: both must drive the same tools in the same order.

### Fixed — two bugs found by driving the built CLI, not by reading it
`--version` printed nothing and **hung**. It was not a known flag, so it fell
through to the default branch, started a stdio server and waited on a stdin that
a human terminal never closes — after opening the user's real
`~/.loopback/loopback.db` on the way. Any typo did the same, silently. argv is
now validated *before* the store is constructed: `--version`/`-v` print the
version and exit 0, anything unrecognised names itself on stderr and exits 1,
and neither touches a database. The e2e check asserts that last part by pointing
the run at a directory that has to stay empty — an exit code alone would not
have caught the database being opened.

A whitespace-only title was accepted while an empty string was correctly
rejected: `" "` is three characters, so it cleared `min(3)`, and the queue
could hold an item nothing could act on. Length checks now run on the trimmed
value across the fields where a blank is meaningless — title, project, claiming
agent, comment author and body, on both submit and update — which also
normalises the padding people paste into slugs instead of storing it.

Both are canaried; the sweep is 30.

### Fixed — smaller corrections
- `docs/05-surface-compatibility.md` stated three different LAN-auth realities
in one document; the bearer token shipped, so the "next security milestone"
text is gone.
- `docs/ROADMAP.md` was stale on its own date, the npm version and the canary
count, and claimed "Open: **Nothing**" while CI was red.
- CHANGELOG dated 0.9.1 to the version-bump commit (2026-07-27) rather than its
actual npm publish (2026-08-01) and registry listing (2026-08-02).
- `integrations/claude.md` labelled an npm install "zero-install from GitHub".
- `server.json` had `websiteUrl: null`, so the registry entry carried no link.
- README overstated capture: buffers hold 30 console lines and 30 network calls,
but a filed report carries the most recent **15 of each**.
- `dashboard/tsconfig.tsbuildinfo`, tsc's incremental cache, was tracked.
- Added `CONTRIBUTING.md`, `SECURITY.md` and issue/PR templates — community
health was 42%.

### Fixed — the hub exited 0 on a taken port
A second `--http --port <taken>` printed its success banner and exited **0**,
having served nothing: express's listen callback fires even when the bind failed
Expand Down Expand Up @@ -39,10 +172,13 @@ remainder:
serialised Error). The cold handshake is guarded, so a dead install is one red
check instead of a lost run that skipped two whole channels.

Sweep is 20. Two of these were caught being decorative by their own canaries
before landing — a port-collision mutation that fell through to a branch whose
message still matched, and a collision probe pointed at a `0.0.0.0` hub from a
`127.0.0.1` client, which never collided at all.
Two of these were caught being decorative by their own canaries before landing —
a port-collision mutation that fell through to a branch whose message still
matched, and a collision probe pointed at a `0.0.0.0` hub from a `127.0.0.1`
client, which never collided at all.

The canary sweep ends this release at **30** checks (20 before the gates above),
and every gate added here is canaried in both directions where it can refuse.

## [0.9.2] — 2026-08-02

Expand Down Expand Up @@ -119,7 +255,7 @@ manifests that do different jobs — `registry.json` for shadcn, `server.json`
for the MCP Registry. Also corrected the widget size, still quoted as
46KB/15KB against a measured 57KB/19KB.

## [0.9.1] — 2026-07-27 — published to npm and the MCP Registry
## [0.9.1] — 2026-08-01 — published to npm; listed on the MCP Registry 2026-08-02

### Added — MCP Registry identity (`mcpName` + `server.json`)
The official MCP Registry proves package ownership by fetching the **published**
Expand Down
Loading
Loading