527983d87ee960caf5f2916c148df9d24ac17dbe
582 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
ba05b2865c |
feat(prime-agent): filterable project picker + a visible active-project indicator
Two gaps the pi switcher had already closed. **The indicator was invisible.** `ctx.ui.setStatus` was the wrong channel: prime-agent stores extension statuses (`FooterDataProvider.setExtensionStatus`) but nothing ever reads them back — there is no `getExtensionStatuses()` call site anywhere in the app, so the value was recorded and never rendered. pi's `footer.js` does render them, which is why the same code showed a status line there and nothing here. The active project is now published as a *widget* (`extensionWidgetsBelow` → `renderWidgets()`), which prime-agent does render. `setStatus` is still called so pi keeps its footer entry. **No search.** With 356 projects the picker was a wall of `smoke-mcptoken-*`. Above 20 projects it now asks for a filter first; space-separated terms must all match, case-insensitively, against name or description, so `home auto` finds `homeautomation`. Active project sorts first, then alphabetical. Deliberately no client-side truncation here, unlike the pi picker: pi's `ExtensionSelectorComponent.updateList()` renders every option, so a long list fills the screen and has to be capped. prime-agent's selector windows the list itself and shows a true `(20/356)` counter — capping would replace an accurate total with a misleading one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017BMXdb2qZbPSh8Q7XpTyjB |
||
|
|
8b006263de |
fix(skills): write the frontmatter every Agent Skills host requires
Some checks failed
CI/CD / lint (pull_request) Successful in 1m18s
CI/CD / typecheck (pull_request) Successful in 1m15s
CI/CD / test (pull_request) Successful in 1m27s
CI/CD / build (pull_request) Successful in 2m17s
CI/CD / smoke (pull_request) Failing after 3m8s
CI/CD / publish (pull_request) Has been skipped
mcpd keeps `name` and `description` as columns, and a skill's `content` is
often just the body — `propose-learnings` starts straight at its `#` heading.
The sync wrote `full.content` verbatim, so the SKILL.md landed with no YAML
frontmatter and the host rejected it. Observed live:
[Skill warning]
~/.prime/agent/skills/propose-learnings/SKILL.md
description is required
Claude Code and pi require the same keys, so this affected every target; it
only surfaced now because prime-agent prints the warning at startup.
`ensureSkillFrontmatter` synthesises the block from the columns when it is
absent, and fills in only a missing `name`/`description` when the author
already supplied a header — an existing complete header is never rewritten.
Values are emitted as JSON strings (valid YAML double-quoted scalars) because
descriptions routinely contain `:` and `#`, which break a bare scalar.
Also: `--force` now re-fetches skills whose server content is unchanged.
Without it a skill already on disk could never be repaired by a client-side
fix — the content hash still matched, so it was skipped forever, which is
exactly what happened on the first attempt to repair the file above.
Three existing assertions compared SKILL.md to the raw server content; they
now assert the body survives rather than exact bytes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017BMXdb2qZbPSh8Q7XpTyjB
|
||
|
|
47809a8942 |
fix(pi): correct the model's tool context after a project switch
Some checks failed
CI/CD / typecheck (pull_request) Successful in 1m13s
CI/CD / lint (pull_request) Successful in 2m30s
CI/CD / test (pull_request) Successful in 1m25s
CI/CD / build (pull_request) Successful in 2m28s
CI/CD / smoke (pull_request) Failing after 2m59s
CI/CD / publish (pull_request) Has been skipped
Switching projects twice left the agent believing it had lost MCP access entirely: it kept calling the previous project's tool names, got "Tool mc_<old>_begin_session not found", and concluded no MCP tools existed. Root cause is a pi constraint, not a bug in the switch. pi has no way to unregister a tool — `registerTool` only ever does `extension.tools.set(name)` — so the previous project's `mc_*` tools stay registered and merely go inactive. `setActiveTools` correctly drops them from the live set, but the conversation still contains the old project's tool listing, so the model keeps calling names that now answer "not found". Nothing was telling the model the tool set had changed. Now the switch injects a custom message naming the active project, stating that previously listed mcpctl tool names are dead, and listing what is actually callable. Custom messages are converted to user-role messages by `convertToLlm`, so they do reach the model (unlike `appendEntry`, which is explicitly excluded from context). `display: false` keeps it out of the transcript — the notification is what the human reads. The gate is also called out explicitly, in both the notification and the injected message. A freshly switched project gets a new mcp-session-id and is therefore gated again, so "1 tool(s) ready" is correct but reads like a failure; it now says which begin_session call unlocks the rest. `toolChangeAnnouncement` is exported and unit-tested rather than left as wording only reachable through a TUI. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017BMXdb2qZbPSh8Q7XpTyjB |
||
|
|
24a3b8cc0a |
feat(pi): filter the project picker instead of scrolling hundreds of rows
Some checks failed
CI/CD / typecheck (pull_request) Successful in 1m10s
CI/CD / test (pull_request) Successful in 1m22s
CI/CD / lint (pull_request) Successful in 2m45s
CI/CD / smoke (pull_request) Failing after 3m35s
CI/CD / build (pull_request) Successful in 2m7s
CI/CD / publish (pull_request) Has been skipped
pi's selector is a plain arrow-key list: `ExtensionUIDialogOptions` has no search field and `ExtensionSelectorComponent` ignores typed characters, so filtering has to happen before the list is handed over. With 356 projects — most of them `smoke-proj-none-*` leftovers — arrowing to the one you want is hopeless. Above 20 projects the picker now asks for a filter first. Terms are space-separated and all must match as case-insensitive substrings, so `home auto` finds `homeautomation`. Blank shows everything, Esc cancels. The active project sorts first (most likely pick), then alphabetical. A result set over 50 is capped, and the title says what was dropped — a silently truncated list reads as "that's all of them". The ordering and matching are extracted into an exported `filterProjects` so they are unit-tested rather than eyeballed through a TUI. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017BMXdb2qZbPSh8Q7XpTyjB |
||
|
|
739e679d1a |
fix(pi): repair the /mcpctl menu, skills target, and typecheck the extension
Some checks failed
CI/CD / lint (pull_request) Successful in 1m7s
CI/CD / test (pull_request) Successful in 1m23s
CI/CD / typecheck (pull_request) Successful in 2m49s
CI/CD / smoke (pull_request) Failing after 1m54s
CI/CD / build (pull_request) Successful in 4m21s
CI/CD / publish (pull_request) Has been skipped
The pi extension shipped in `src/pi-ext/` was covered by no tsconfig and no
eslint config, so nothing ever checked it against pi's API. Pointing tsc at
the published @earendil-works/pi-coding-agent types found the command surface
to be inert.
Fixes:
- `/mcpctl` did nothing. `ctx.ui.select` takes `string[]` and returns the
chosen string; it was called with `{value,label}` objects, so the menu
rendered five `[object Object]` rows and `choice === "status"` never
matched any branch. Labels are now plain strings mapped back to actions.
- The headless branch returned a status string from a handler typed
`Promise<void>`; pi drops it. Reports via notify instead.
- "Sync skills" omitted `--agent pi`, writing into ~/.claude/skills — in an
integration whose stated purpose is to not depend on ~/.claude — and said
so in its own success message. It also ran execSync with `stdio: "inherit"`,
painting raw output over pi's TUI, and interpolated the project name into a
shell string. Now execFile with `--agent pi` and captured output.
- Tool results typed `content[].type` as `string`; pi's AgentToolResult wants
the `"text"` literal.
- `callTool` asserted `Promise<unknown>` to `ToolCallResult`.
- Sanitising MCP tool names to `[a-z0-9_]` can collide (`docs.search` vs
`docs-search`). The colliding tool was silently never registered but still
reported active, so its calls were forwarded to the first tool. Names are
now disambiguated and tracked with the MCP tool they forward to.
- `registerWithPi` rewrote settings.json even when nothing changed. Since
parsing strips `//` comments, a no-op run destroyed them.
Guards, so this class of bug can't return:
- `src/pi-ext/tsconfig.json` checks the extension against the real published
pi types (dev dependency, not a shim — a shim drifting from the published
API is the exact failure being guarded). Wired into `pnpm typecheck`.
- eslint now covers `src/pi-ext/*.ts` like every other source file.
- A test fails if the embedded copy in `config/pi-extension.ts` is stale;
editing the sources without regenerating silently shipped old code.
Also: the branch added `config pi` without regenerating shell completions
(the committed-completions test was failing), and the doc advertised
`mcpctl pi sync-skills`, which does not exist. Both corrected, plus a note
on the session-token vs `mcpctl_pat_` bearer difference that would bite
against an authenticated `mcplocal serve`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017BMXdb2qZbPSh8Q7XpTyjB
|
||
|
|
f1d84b0952 |
fix(config pi): embed extension sources so installed binary is self-contained
- add scripts/generate-pi-extension.ts to embed src/pi-ext sources as string constants (mirrors prime-agent's embedded switcher pattern). - config pi now writes the embedded sources by default, so it works from an /usr/bin install with no source tree (--extension-dir overrides for dev). Verified installed binary writes files byte-identical to source. - add installEmbeddedExtension test. |
||
|
|
772c25f605 |
feat(cli): make the agentic-teams stack usable from the terminal
Everything so far was API-only. This adds the CLI surface and proves the whole
vertical works end to end against a real mcpd and a real Gitea.
mcpctl create gitprovider mysources --url https://mysources.co.uk \
--secret gitea-token --key TOKEN
mcpctl repos discover mysources --query mcp --project demo
mcpctl repos import mysources --project demo michal/mcpctl
mcpctl create epic ship-agents --project demo --content "..." \
--criterion "cli=CLI can drive a task" --criterion "q:feel=reads at a glance"
mcpctl create epictask foundation --epic ship-agents
mcpctl create task wire-cli --epictask foundation --title "Wire the CLI"
mcpctl tasks advance wire-cli in_progress
mcpctl tasks verdict wire-cli --stage qa_review --commit 3f2ab19 --pass --summary "..."
mcpctl tasks accept wire-cli --note "ship it"
mcpctl tasks history wire-cli
`repos` and `tasks` are their own verb groups rather than more create/edit
flags. Discovery is inherently two-step — look at what is there, then pick — and
accept/reject are the human gate, not field writes.
`get` gained columns for all five resources. The Epic row shows completeness as
"40% (2/5)" and CodeRepo shows SOURCE/FROM, so imported repos say where they
came from at a glance.
BREAKING: `mcpctl get tasks` now lists WORK ITEMS. The inference queue keeps
`inference-task(s)` and gains `itask(s)`; the short `task`/`tasks` aliases move
to work items. This completes the split already made in RBAC (
|
||
|
|
b70300d389 |
feat(mcpd): CodeRepo + GitProvider — register a forge, discover and import repos
Two resources landing together, because a repo you have to hand-type is a repo nobody adds. CodeRepo is a git repository attached to a project. GitProvider is a configured forge instance you register once — URL and credential in one place — and then list and import from. Nothing above the driver layer knows what a Gitea is. src/mcpd/src/services/git/ holds the GitProviderDriver interface, the Gitea implementation and a registry that resolves by type, mirroring LlmAdapterRegistry.build. Adding GitHub or GitLab is one file plus one case. The instance URL always comes from the GitProvider row; there is no hardcoded host anywhere. Follows the mcpservertemplates convention: a declarative resource applied via apply -f, with instances keeping denormalized provenance that outlives the source. CodeRepo stores sourceProvider + sourceFullName alongside the FK, so "where did this repo come from" stays answerable after the forge is disconnected — the same reasoning as McpServer.templateName/templateVersion. The FK is SetNull, so disconnecting a forge never deletes the repos, or the tasks hanging off them, that came from it. Discovery uses /repos/search, deliberately NOT /user/repos. The latter needs the read:user scope, which repo-scoped automation tokens do not carry — the token in this repo's .env has write:repository and 403s on /user/repos. The driver also passes Gitea's 403 scope message through verbatim, so an under-scoped token says which scope it is missing instead of looking like an empty forge. Import is partial-success by contract: one bad name must not lose the other nineteen. Everything importable is imported; everything skipped comes back with a reason, and a re-import is distinguished from a name collision with an unrelated repo. Imported repos store no credential of their own and inherit the forge's, so rotating one token covers every repo from that forge. Credentials never leave as plaintext: reads return the reference only, and resolveCheckouts() — the single path that produces a token — exists for the session provisioner and is called by no route. References are verified at definition time, so a wrong key fails with "secret has no key 'X'" rather than as a clone that mysteriously cannot authenticate. Clone URLs must be https/ssh/scp-style and checkout paths must be relative with no ".." segment: both are inputs to the clone step, and neither should be able to point it at mcpd's own filesystem or escape the workspace. Tests: 18 CodeRepo + 22 GitProvider unit tests. Gitea driver additionally verified live against https://mysources.co.uk (Gitea 1.25.4, 10 repos discovered, query filter working). Local: build clean, workspace 2440 passed (+40), db 128 passed (same 4 pre-existing Secret failures), lint unchanged at 869. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N4wNHWf7xSwnZCWpJcyv9p |
||
|
|
f90e23d000 |
Merge remote-tracking branch 'origin/main' into feat/pi-extension
# Conflicts: # README.md # src/cli/src/commands/config.ts # src/cli/src/commands/skills.ts |
||
|
|
05abe3522c |
feat(mcpd): Task lifecycle — CAS transitions, risk-scaled done gate, accept gate
The work-item spine: repository, validation, service and routes for Task, plus its verdicts and append-only transition log. Transitions are compare-and-swap. The expected current state is part of the WHERE clause and the affected-row count is the answer, so two agents racing to advance the same task cannot both win — a read-then-write would let both "succeed" and silently lose one. Losing the race is a 409 naming the state the task is actually in, not a 500, because on a shared board that is a normal outcome the caller retries. Same idiom as InferenceTaskRepository's claim. QA / Security / SRE are stages inside the task's own loop, not post-submit reviewers, so each can bounce straight back to in_progress — that is what "feedback" means here. A failed build likewise goes backward, not forward. `merged` allows only `reopened`, so a merged task cannot be quietly edited; post-merge failure is the one way back in. The gate on `done` scales with risk instead of running a fixed pipeline: trivial needs nothing, normal needs a passing QA verdict, high needs QA + Security + SRE. Forcing a one-line docs fix through security review is how a process gets routed around, so the cost has to match the risk. A FAILING verdict does not clear a gate — a recorded failure is evidence the gate was tried and did not hold. The 400 names which gates are still missing. The human accept gate is a task-level yes/no on a resolved outcome, not per-tool approval: accept fires the merge and stores the comment, reject sends it back to in_progress and REQUIRES a comment, because the comment is the direction the agent works from. GET /tasks/:id returns `legalTransitions` so no client has to hardcode the table. Tests: 25 service tests — full happy path, every stage's bounce-back, illegal and no-op transitions, the CAS race (including that no event is written when the race is lost), the complete risk matrix including failing-verdict-does-not- count, and the accept/reject gate. Local: build clean, workspace 2400 passed (+25), db 128 passed (same 4 pre-existing Secret failures), lint unchanged at 869. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N4wNHWf7xSwnZCWpJcyv9p |
||
|
|
28f1a411fd |
feat(pi): add native pi integration — extension, config pi, skills --agent pi
- src/pi-ext/: self-contained pi extension (mcpctl-pi.ts + vendored mcp-http client) that talks JSON-RPC directly to mcplocal and registers project MCP tools as native pi tools. No MCP client, no ~/.claude. - Persistent per-project session so gated projects ungate on begin_session. - /mcpctl command: status, switch project (GUI), refresh tools, sync skills. - mcpctl config pi: installs extension, wires pi settings, persists active project, syncs skills into ~/.pi/agent/skills. - skills sync: add --agent pi (target install root). - docs + tests. |
||
|
|
3f93f0798f |
Merge pull request 'feat(cli): show the active mcpctl project in prime-agent's footer' (#96) from feat/mcpctl-switcher-status into main
Some checks failed
|
||
|
|
ce7df10e06 |
feat(cli): show the active mcpctl project in prime-agent's footer
`/mcpctl` could switch projects but there was no way to see which one was active without running a command. prime-agent exposes the same status-bar channel the model name uses (`ctx.ui.setStatus`), so the switcher now publishes `mcpctl:<project>` there. Wired to `session_start`, which fires on startup *and* on every reload — including the reload the switch itself triggers — so the footer tracks settings.json without extra bookkeeping. Cleared when no project is mounted. Also fixes `notify(..., 'success')`: the API only accepts info|warning|error. Not a live bug (prime-agent falls through to the same showStatus path as 'info') but it fails a typecheck of the extension against the real ExtensionAPI, which is how it was found. The extension ships as a JSON-escaped string and is never compiled by our build, so it was typechecked out-of-tree against @earendil-works/pi-coding-agent's published types. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017BMXdb2qZbPSh8Q7XpTyjB |
||
|
|
25c1bffdd0 |
Merge pull request 'fix(cli): harden config prime-agent sync, auth + /mcpctl switcher' (#95) from feat/config-prime-agent into main
Some checks failed
|
||
|
|
170dc06496 |
fix(cli): close third review — token collision, migration, ownership
Some checks failed
CI/CD / typecheck (pull_request) Successful in 1m5s
CI/CD / lint (pull_request) Successful in 2m13s
CI/CD / test (pull_request) Successful in 1m20s
CI/CD / build (pull_request) Successful in 2m9s
CI/CD / smoke (pull_request) Failing after 2m44s
CI/CD / publish (pull_request) Has been skipped
Round 2 fixed the first review but introduced regressions of its own, all of which only bite against state written by the previously installed build. `config prime-agent`: - Mint each credential under a unique `prime-agent-<stamp>` name again. `McpToken` is unique on (name, projectId) and revoke is a soft delete, so round 2's fixed `prime-agent` name could only ever be minted once per project — and the revoke-first ordering destroyed the working credential before discovering the mint would fail. - Provision the credential BEFORE touching settings.json. Registering the new project unmounts the previously active one, so a failed mint must not be able to leave prime-agent with no working project at all. The command now aborts with settings.json untouched. - Retire only the token this auth.json actually held, once its replacement is stored. Sweeping every `prime-agent*` token for the project would revoke the credential another install (or a custom --output run) is using; anything else that looks orphaned is reported, not deleted. - Validate a pre-existing credential instead of trusting its presence: a revoked or expired token used to short-circuit provisioning and leave prime-agent broken while the command reported success. Matched by tokenPrefix against the project's active tokens, so the secret is never sent. Fails open when the API can't be consulted. - Actually write auth.json 0600. `writeFile`'s mode is ignored for an existing file and prime-agent creates auth.json itself at 0644, so chmod after writing. - Recognise the untagged mcpServers entries older CLIs wrote (canonical proxy URL + an `mcp:<name>` mcpctl PAT in auth.json) so a switch unmounts them instead of leaving two gateways live. Hand-configured servers have no such credential and are still preserved. Same rule in the `/mcpctl` switcher's active-project lookup. - Add `--skip-marker`, and pass it from the `/mcpctl` switcher: the extension runs from whatever directory prime-agent was started in, and was silently re-scoping that repo's `.mcpctl-project`. `skills sync --agent prime-agent`: - Record ownership from the skill's own scope, not the syncing project's. Globals were being pinned to whichever project happened to sync them, after which every other project refused to update them forever. - Never adopt legacy, ownership-less state into the current scope. Round 2 did, which deleted the other project's skills on the first sync after upgrading. Such entries are attributed to the project that last wrote the state file, and left alone when that isn't the project syncing now. - Close the overwrite-guard bypass: a sync with no project, or a global landing on a project-owned name, could still clobber and re-own a tracked skill. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017BMXdb2qZbPSh8Q7XpTyjBbench-cpa-fixed |
||
|
|
da4752b90f |
fix(mcplocal): stop the 30s proxy timeout killing agent turns
`mcpctl chat <agent>` failed with
HTTP 503 {"error":"service_unavailable","message":"Cannot reach mcpd daemon. Is it running?"}
while mcpd was answering /healthz in 32ms. The message was wrong in a way that
cost real debugging time: mcplocal was reaching mcpd fine and giving up after
30s. journalctl shows the signature plainly — statusCode 503 with
responseTime 30003.87 on POST /api/v1/agents/reviewer/chat.
This blocks the agentic-teams epic outright. An agent turn is a multi-turn
tool-use loop that runs for minutes by design, so a 30s ceiling on the chat path
is not a safety net, it is a guaranteed failure for every non-trivial turn.
Three defects, all in the same path:
1. One blanket budget for every forwarded route. DEFAULT_TIMEOUT_MS = 30_000 is
right for CRUD and wrong for chat. Chat, project chat, llm infer and
inference-task streams now get LONG_RUNNING_TIMEOUT_MS (600_000, override
with MCPLOCAL_LONG_TIMEOUT_MS) — matching STREAM_TIMEOUT_MS, which the CLI
already allowed. mcplocal in the middle was the binding constraint.
2. Timeouts were reported as connection failures. Split UpstreamTimeoutError
out of ConnectionError and map it to 504 with an accurate message that says
the daemon IS reachable. ConnectionError still means unreachable and still
returns 503. Verified nothing else branches on ConnectionError.
3. SSE was buffered. `forward()` reads the whole body through res.text(), so
even turns that finished in time arrived as one blob and the CLI's live
token output never appeared. Streaming routes now use forwardStream() and
pipe the body straight through, preserving content-type and
x-accel-buffering (dropping the latter lets intermediaries re-buffer and
reintroduces the stall).
Also closes the escape that produced the sibling `500 code:23` failure: the body
read in forward() was outside the try, so when mcpd had already written SSE
headers the raw DOMException reached Fastify unhandled.
Tests: 9 new proxy tests. The two that matter — "does not abort an agent chat
that outlives the CRUD budget" and "streams SSE through instead of buffering" —
were confirmed to FAIL against the pre-fix behaviour and pass after. Three
existing mcpd-client tests asserted the old taxonomy and were updated to assert
the new one deliberately.
Local: build clean, workspace 2375 passed, lint unchanged at 869.
NOT YET LIVE: mcplocal runs from the installed RPM, so this needs a package
rebuild + `systemctl --user restart mcplocal` to take effect.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N4wNHWf7xSwnZCWpJcyv9p
|
||
|
|
fa7055ac5e |
fix(cli): close second review on prime-agent sync + switcher (auth, ownership, switching)
Some checks failed
CI/CD / lint (pull_request) Successful in 1m4s
CI/CD / typecheck (pull_request) Successful in 1m5s
CI/CD / test (pull_request) Successful in 3m7s
CI/CD / smoke (pull_request) Failing after 3m13s
CI/CD / build (pull_request) Successful in 2m12s
CI/CD / publish (pull_request) Has been skipped
Addresses the second round of `config prime-agent` review (10 findings). auth.json (config/prime-agent.ts) — the settings.json data-loss fix had a twin: - loadPrimeAgentAuth now fails loudly on corrupt JSON instead of swallow-and- rewrite, so one syntax error can no longer destroy the provider API key and every other project's credential. hasPrimeAgentAuth shares that guarantee. - writePrimeAgentAuth writes 0600 (preserving an existing file's mode) instead of the default umask — bearer tokens are no longer world-readable on first creation. state ownership (commands/skills.ts) — the ownership model edge cases: - orphan-removal guard now normalises a canonical scope (project name, or null for globals; legacy undefined adopted to current scope) instead of comparing null against undefined, so global-only syncs and pre-PR state can no longer leave stale skills on disk forever. - a same-named skill *tracked* to a different project is preserved (with a warning) rather than silently overwritten in the shared flat tree. - mcpServers auto-attach is gated behind !isPrimeAgent with an explicit warning (prime-agent's HTTP gateway must not mutate shared mcpd project attachments); this also makes the earlier dropped-attach concern explicit rather than silent. single-active project + switcher (config/prime-agent.ts, prime-agent-extension.ts): - registerPrimeAgentMcp tags the project's entry mcpctlManaged:true and removes other mcpctl-managed entries, so prime-agent has one *active* mcpctl project while preserving untagged servers (hand-configured sre, websearch, etc). - the /mcpctl extension now reads that tag as the single source of truth for the active project, fixing the false short-circuit / no-op switch. config.ts command: - an explicit -p now updates a differing up-tree .mcpctl-project marker (scope no longer silently reverts on the next sync), no-ops when it matches, and still never scopes $HOME. - a project left with no usable credential now exits non-zero (the /mcpctl extension checks child exit status, so it no longer reports a successful switch after provisioning failed). - skills sync is treated as best-effort: settings+auth determine switch success, so a skills error no longer falsely fails the switch. - token minting now revokes prior active `prime-agent` tokens before creating a fresh one (no more never-expiring token litter / lost-credential duplication). Tests (544 green): corrupt auth.json refusal, 0600 mode, mint-failure exit code, single-active dedup preserving untagged sre, cross-project overwrite preservation, and global-orphan removal on global-only sync.bench-cpa-pre-fix |
||
|
|
7f64316d59 |
feat(mcpd): EpicTask resource — roadmap nodes, edges, and the roadmap view
Roadmap nodes plus the edges that chain them into the dependency graph the design doc calls the spider web. Edge direction is "from depends on to". The database cannot enforce graph integrity here — production provisions with `prisma db push`, so a trigger or CHECK would never exist there, and the schema test added earlier documents that boundary explicitly. So the service rejects the three things that would otherwise corrupt the roadmap: - self-edges, - edges crossing Epics, since a roadmap is one Epic's graph, - cycles, which make the graph unschedulable and hang any topological walk. Cycle detection is a BFS over the prerequisite direction: adding from→to closes a loop exactly when `to` already depends transitively on `from`. The error names the offending path, not just "cycle", so a failed apply says which nodes to look at. The traversal carries a seen-set that also guarantees termination if the stored graph ALREADY contains a cycle — from a hand-edited database or an earlier bug — rather than spinning forever. There is a test for that case. Adding an existing edge is a no-op rather than an error, so applying a manifest twice is safe. GET /api/v1/epics/:id/roadmap returns nodes with per-node completeness, both edge directions, and `ready`: the nodes whose prerequisites are all satisfied, which is the question anyone actually asks of a roadmap. A cancelled prerequisite counts as satisfied — it is not coming. Completeness reuses EpicService.completeness verbatim so a node and its Epic can never disagree about what "met" means. Tests: 19 service tests — the full cycle matrix (self, 2-cycle, long transitive, diamond-is-fine, pre-existing-cycle termination), roadmap edge directions, readiness including the cancelled-prerequisite case, and CRUD guards. Local: build clean, workspace 2366 passed (+19), db 128 passed (same 4 pre-existing Secret failures), lint unchanged at 869. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N4wNHWf7xSwnZCWpJcyv9p |
||
|
|
909506317e |
feat(mcpd): Epic resource — the vision layer, with an honest-done gate
Full CRUD for Epic: repository, zod validation, service, routes, main.ts wiring, and revision tracking (ResourceType gains 'epic', which is a plain string union by design so this needed no migration). The substance is the done rule, not the CRUD. An Epic exists to stop "every task completed, still no car", so status is deliberately NOT settable through PUT — it goes through POST /epics/:id/status, which enforces: - `delivered` requires every done-criterion met. The 400 names the unmet ids (truncated past 5) so the caller knows what is actually missing. - `delivered` with recorded tradeoffs is refused — if you accepted a gap, the status has to say so. - `delivered_with_tradeoffs` requires at least one recorded tradeoff, otherwise it is `delivered` wearing a disguise. - `delivered` with zero criteria is refused: nothing was claimed, so nothing can have been delivered. - `cancelled` and `open` are always allowed — abandoning needs no ceremony, and reopening is how a gap gets fixed. Completeness counts `quality` criteria alongside `outcome` ones. Grading only the mechanically checkable half is exactly the failure the Epic exists to prevent, so both feed the percentage, and it is computed server-side and attached to every response rather than left for clients to re-derive. Tradeoffs are append-only and stamped with who and when — the record is the audit trail for why "done" was honest. Scope changes patch-bump semver, so creep is versioned as visibly as under-delivery. Tests: 26 service tests (the full done-rule matrix, tradeoff append/stamping, criterion flipping, semver bumping, and that a plain update cannot smuggle a status through) and 11 route tests (completeness attachment, owner stamping, 400 vs 404 vs 500 behaviour, the status gate over HTTP). Local: build clean, workspace 2347 passed (+37), lint unchanged at 869. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N4wNHWf7xSwnZCWpJcyv9p |
||
|
|
ffcd0f985b |
feat(orchestrator): ephemeral session pods — restart policy, volumes, init containers
An MCP server container is long-lived and restarts forever; an agent session pod is the opposite — it does a unit of work and dies. ContainerSpec could not express that: restartPolicy was hardcoded 'Always' in generatePodSpec, and there were no volume, workdir, or init-container fields at all. Adds four optional fields to ContainerSpec (restartPolicy, workdir, volumes, initContainers) and implements them in both backends. All optional, so every existing caller is byte-identical — generatePodSpec still emits 'Always' unless asked otherwise, and omits volumes/initContainers keys entirely when unused. Kubernetes gets this nearly free: initContainers run to completion in declaration order before any app container starts, so "clone the repos, then work them" needs no sequencing logic from us. generateDeploymentSpec deliberately ignores spec.restartPolicy — k8s rejects anything but 'Always' on a Deployment pod template — and says so in a comment. Docker has no init containers, so they are emulated as sequential runs sharing the same volumes. The important property is the failure path: a non-zero init exit aborts BEFORE the main container is created, and the error carries the init container's last 50 log lines. Otherwise a failed git clone would leave an empty workspace that looks exactly like a successful one, and the agent would start work against nothing. Named Docker volumes are namespaced by container name to stand in for emptyDir. Tests: 10 k8s manifest tests (defaults unchanged, each restart policy, volume and mount emission, init ordering, name sanitization, security context still hardened) and 9 Docker tests including the abort-before-main case and log propagation. Local: build clean, workspace 2310 passed (+19), lint unchanged at the repo's pre-existing 869. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N4wNHWf7xSwnZCWpJcyv9p |
||
|
|
fceb3cfb95 |
feat(db): agentic-teams data model — epics, work items, repos, membership
Adds the foundation entities from docs/agentic-teams.md §5/§7: a versioned vision layer (Epic -> EpicTask -> Task), git repositories attached to projects, agents as many-to-many project members, and the ephemeral session that IS an agent's workspace. 9 tables + 5 enums, purely additive — no drops and no column changes on existing tables, since every new relation is held on the new side. Shape notes: - EpicTask chaining uses an explicit EpicTaskEdge table rather than a self-referencing array, so the roadmap graph walks in both directions and edges can carry a type later. - TaskVerdict is commit-pinned and immutable: "QA verified 3f2ab" stays reproducible, which is what lets later coverage claims be audited rather than believed. TaskEvent is the append-only transition log. - Task survives losing its repo and its assignee (both SetNull) — a work item outlives whoever happened to be working it — but cascades from its EpicTask. - CodeRepo, not Repository: `repository.repository.ts` next to 24 other *.repository.ts data-access files would be actively confusing. CLI resource is `repos`, with repo/repository/repositories aliased onto it. - AgentProject.llmId exists so the brain-ownership question (docs/agentic-teams.md §15 Q1) can be settled either way without a migration. - Agent.projectId is retained and coexists with AgentProject for one release so findByProjectId / prepareContext keep working. Note this partially reverses 20260223120000_remove_project_members — deliberately, and only for agents; human membership stays RBAC-driven. No CHECK constraints anywhere, on purpose: deploy/entrypoint.sh provisions with `prisma db push`, so hand-written migration SQL never runs in production. The migration file is kept for local `migrate dev` parity and as a readable record, and says so in its header. Invariants that cannot be expressed in the schema (EpicTaskEdge cycle rejection) belong to the service layer; a schema test documents that boundary rather than pretending the database enforces it. Tests: 23 schema tests covering defaults, per-project name scoping, cascade vs SetNull on every FK, all 12 lifecycle states, and the GC-sweep predicate. clearAllTables extended for all 9 models. Verified locally against postgres:16-alpine on :5433 — db suite 128 passed (baseline was 105; the same 4 pre-existing Secret_backendId_fkey failures remain, unrelated). Workspace 2291 passed, build clean, lint unchanged at the repo's pre-existing 869. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N4wNHWf7xSwnZCWpJcyv9p |
||
|
|
eb1642ab1a |
fix(cli): harden config prime-agent sync + install /mcpctl switcher extension
Some checks failed
CI/CD / lint (pull_request) Successful in 1m4s
CI/CD / typecheck (pull_request) Successful in 1m6s
CI/CD / test (pull_request) Successful in 3m9s
CI/CD / build (pull_request) Successful in 2m16s
CI/CD / smoke (pull_request) Failing after 3m27s
CI/CD / publish (pull_request) Has been skipped
Addresses a review of the `config prime-agent` feature and adds the in-app project switcher. Safety/correctness fixes (prime-agent's shared, hand-editable ~/.prime/agent tree must never suffer silent data loss): - config/prime-agent.ts: loadPrimeAgentSettings now fails loudly on corrupt JSON instead of swallowing it and rewriting the file (which destroyed every non-mcpServers setting). A project's mcpServers entry is merged (keeping user-added fields) rather than replaced wholesale. Added writePrimeAgentAuth / hasPrimeAgentAuth helpers for auth provisioning. - skills sync: unified the near-verbatim prime-agent copy into runSkillsSync via a `target: 'claude' | 'prime-agent'` option (prime-agent-skills.ts is now a thin wrapper). Under the prime-agent target it: preserves untracked pre-existing skill dirs on first sync (no more rm -rf of hand-authored `sre`), records per-project ownership so configuring a second project never deletes the first project's skills, skips Claude-only hooks/postInstall, and keeps the mcpServers auto-attach step. - config.ts: `config prime-agent` now (a) provisions the bearer credential in auth.json (--token, existing entry, or auto-mint via POST /api/v1/mcptokens), (b) writes the .mcpctl-project marker only when none exists up-tree and never from $HOME, and (c) propagates the skills sync exit code so auth failures are reported instead of swallowing them. - skills.ts: `--agent` is validated; an unknown value errors instead of silently running the Claude sync. New feature: `config prime-agent` installs a `/mcpctl` project-switcher extension into ~/.prime/agent/extensions/ (skip with --skip-extension). It lists mcpctl projects via `mcpctl get projects -o json`, lets you pick one from the prime-agent TUI, applies the switch through the CLI, and reloads the session. Regenerated shell completions. Tests: 538 pass (new coverage for settings corruption, entry merge, auth provisioning, extension install/skip, marker $HOME handling, untracked/cross-project skill preservation, --agent validation). |
||
|
|
a186d95a85 |
feat(rbac): split the tasks namespace into tasks + inference-tasks
v5 gave the durable inference queue the RBAC resource `tasks`, on the reasoning that inference was the only kind of task we had. Agentic teams introduces work-item tasks (Epic -> epictask -> task), which need that name. The queue moves to its own `inference-tasks` resource, matching its existing /api/v1/inference-tasks route prefix, and `task`/`tasks` now mean work items. Also lands the agentic-teams aliases: epic, epictask/et, repo/repository, session. The rename is not purely a code change. ResourceBindingSchema runs normalizeResource at parse time and stores the normalized string, so every RbacDefinition row written before the split literally contains "tasks" meaning inference. Shipping the alias change alone would silently stop those grants authorizing the queue and start them authorizing work items -- a privilege change in both directions. bootstrap/rbac-tasks-backfill.ts rewrites them once at startup, alongside migrateAdminRole. The run-once marker is claimed BEFORE the rewrite, not after. A failed marker write after a successful rewrite would let the next startup rewrite again, converting genuine post-split work-item grants into inference grants. Losing the rewrite is recoverable by re-granting; running it twice silently is not -- so if the marker cannot be persisted we skip the rewrite and warn loudly. mapUrlToPermission moves in lockstep: /api/v1/inference-tasks maps to `inference-tasks`, otherwise the split would break queue authorization on the first request. Adds generic getBooleanSetting/setSetting helpers to bootstrap/system-settings.ts, following the existing "global settings live in a Secret" pattern. setSetting refuses to write when the settings secret has been migrated onto a non-plaintext backend, where a direct `data` write would read back empty. Out of scope, tracked separately: the CLI still routes `mcpctl get tasks` to the inference queue (src/cli/src/commands/shared.ts). Repointing it needs the work-item resource to exist first, or the command breaks with no replacement. Tests: 14 unit tests cover alias resolution, the rewrite, wildcard and operation bindings being left alone, idempotency, and both marker-failure paths. Full workspace suite green (2291 tests). The new smoke test is RED until mcpd is deployed with this change -- it asserts post-split storage against a live daemon, and the currently deployed one predates the split. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N4wNHWf7xSwnZCWpJcyv9p |
||
| d56a074733 |
Merge pull request 'feat(cli): add mcpctl config prime-agent — proxy MCP + skills sync for prime-agent' (#93) from feat/config-prime-agent into main
Some checks failed
Reviewed-on: #93bench-cpa-base |
|||
|
|
797df2299d |
docs(taskmaster): PRD + task graph for agentic teams foundation
Converts docs/agentic-teams.md §16 steps 1-3 into a trackable PRD and 48 appended tasks (IDs 115-162) in the master tag. Steps 4-8 land as 5 low priority placeholders wired onto the spine, pending the §15 open questions. The PRD is grounded in the current codebase rather than the design doc's indicative sketches, which changed the shape of the work: - RBAC `tasks` is already owned by the inference queue, and bindings are normalized at parse time, so existing RbacDefinition rows literally hold "tasks". Taking the name for work items needs a one-shot guarded backfill to `inference-tasks` (task 116). - deploy/entrypoint.sh uses `prisma db push`, so migration SQL never runs in production. Invariants are service-layer, not CHECK constraints. - Ephemeral pods do not exist: restartPolicy is hardcoded Always and ContainerSpec has no volume/workdir fields (tasks 140-142). - `exec` is cheap: KubernetesOrchestrator already implements exec/attach over k8s.Exec/k8s.Attach; only an API + CLI surface is missing (tasks 144-145). - No git provider client exists in product code (task 148). - ChatService caps its tool loop at 12 iterations, so the AgentRuntime runs a headless coding agent in-pod rather than extending chat (tasks 146-147). Names the git-repo entity CodeRepo (CLI `repos`) to avoid colliding with the repository/ data-access layer; AgentProject carries an optional llmId so the brain-ownership question can be settled either way without a migration. Generated task_*.md files are left untracked, matching how this repo has treated them since the first commit. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N4wNHWf7xSwnZCWpJcyv9p |
||
|
|
582f6f185b |
feat(cli): add mcpctl config prime-agent — proxy MCP + skills sync for prime-agent
Some checks failed
CI/CD / lint (pull_request) Successful in 1m6s
CI/CD / typecheck (pull_request) Successful in 2m10s
CI/CD / test (pull_request) Successful in 1m19s
CI/CD / smoke (pull_request) Failing after 1m53s
CI/CD / build (pull_request) Successful in 4m14s
CI/CD / publish (pull_request) Has been skipped
Mirror `mcpctl config claude` for prime-agent (which talks to the same mcpctl
proxy MCP gateway over HTTP instead of stdio):
`mcpctl config prime-agent --project X`:
- registers the proxy MCP gateway in ~/.prime/agent/settings.json as
mcpServers.X = { type: "http", url: <gateway>/projects/X/mcp }, merging with
any existing servers (e.g. the bundled `sre` project) and preserving all other
settings
- writes a .mcpctl-project marker so later syncs resolve the project
- syncs the project's skills into ~/.prime/agent/skills/<name>/ as markdown
skills (prime-agent auto-discovers them at session start)
New `mcpctl skills sync --agent prime-agent` target re-syncs the tree later.
- src/cli/src/config/prime-agent.ts: settings.json read/merge/write helpers
- src/cli/src/utils/prime-agent-skills.ts: prime-agent sync (reuses
installSkillAtomic + skills-state; skips Claude-only hooks/postInstall)
- completes config.ts/skills.ts wiring; regenerated shell completions
- tests: commands/prime-agent.test.ts + utils/prime-agent-skills.test.ts
|
||
|
|
d24c30761b |
docs: keep superseded design-round doc history
Supporting documents folded into agentic-teams.md (the canonical unified design record), retained as historical context: - agentic-teams-review.md (round 2) - vision-fidelity.md (round 3 diagnosis + research) - epic-task-artifact-model.md (Epic/epictask/task + context philosophy) - session-summary-vision-fidelity.md (session narrative) All superseded by docs/agentic-teams.md, which remains the source of truth. |
||
|
|
5e1c6b3681 |
docs: unified agentic-teams design document
Consolidates agentic-teams (rounds 1-2), vision-fidelity, Epic/artifact model, and session summary into one canonical design record. Supersedes: agentic-teams-review.md, vision-fidelity.md, epic-task-artifact-model.md, session-summary-vision-fidelity.md |
||
| 2c8419eddb |
Merge pull request 'fix(cli): stop the MCP stdio bridge serialising requests' (#92) from fix/mcp-bridge-head-of-line into main
Some checks failed
One slow call stalled every later request because the stdin loop awaited each in turn; queued requests were never sent so nothing could time them out and the client saw silence. Dispatch is now concurrent. Regression test verified to fail on the old code. |
|||
|
|
e85250fedf |
fix(cli): stop the MCP stdio bridge serialising requests
Some checks failed
CI/CD / lint (pull_request) Successful in 1m7s
CI/CD / typecheck (pull_request) Successful in 2m11s
CI/CD / test (pull_request) Successful in 1m20s
CI/CD / build (pull_request) Successful in 2m24s
CI/CD / smoke (pull_request) Failing after 3m23s
CI/CD / publish (pull_request) Has been skipped
The bridge's stdin loop awaited every request before reading the next line, so it handled exactly one at a time. A single slow call therefore stalled every later request — and because those requests were never even sent, nothing could time them out. The client saw silence, not an error. Observed 2026-08-05: two gitea calls through this bridge sat completely mute until Claude Code aborted them at its own 1800s idle limit, reporting "sent no response or progress". The upstream was healthy the whole time — a fresh session answered the same tools in 0.3s, and the gitea MCP server's own log showed the calls never reached it. They died queued in the bridge. JSON-RPC ids exist precisely so responses may return out of order, so nothing here needed a queue. Requests now dispatch concurrently and are tracked in a set; stdin close awaits them before the session DELETE, otherwise a concurrent call races the teardown and dies with a 404. We still serialise until the session id exists: it arrives on the first response, and firing later requests without it would open a second upstream session. A client sends `initialize` first and waits for the reply anyway, so this costs one round trip rather than throughput. Also makes the per-request timeout configurable via MCPCTL_MCP_TIMEOUT_MS (default unchanged at 30s) and names it in the timeout error, so a project with genuinely long tool calls can raise it instead of hitting a hardcoded wall. Tests pin both halves: a fast request must overtake a slow one (this fails on the old serial code — verified by reverting), and a failed request must still produce a JSON-RPC error carrying the original id rather than nothing. |
||
| cf85e36ede |
Merge pull request 'feat(gate): drop reasoning field from selection (~1.5s)' (#90) from feat/gate-drop-reasoning into main
Some checks failed
|
|||
|
|
34e00af731 |
feat(gate): drop the reasoning field from selection → ~1.5s (comfortable margin)
Some checks failed
CI/CD / lint (pull_request) Successful in 1m6s
CI/CD / typecheck (pull_request) Successful in 2m12s
CI/CD / test (pull_request) Successful in 1m20s
CI/CD / build (pull_request) Successful in 2m11s
CI/CD / smoke (pull_request) Failing after 2m47s
CI/CD / publish (pull_request) Has been skipped
Even "<=8 words" was ignored — the model wrote a full-sentence reasoning, pushing
begin_session to ~7s (thin under the 8s budget). The gate only needs the names, so
request just {"selectedNames":[...]}. Validated live vs vllm-current
(glm-4.6-reap-fast): 1.5s, valid JSON. (reasoning defaults to '' in extractSelection.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
| 2f9e7be762 |
feat(gate): terse selection prompt (~2s) (#89)
Some checks failed
|
|||
|
|
a432e9e19f |
feat(gate): terse selection prompt so a no-think model answers in ~2s
Some checks failed
CI/CD / lint (pull_request) Successful in 1m3s
CI/CD / typecheck (pull_request) Successful in 2m10s
CI/CD / test (pull_request) Successful in 1m21s
CI/CD / build (pull_request) Successful in 2m14s
CI/CD / smoke (pull_request) Failing after 3m16s
CI/CD / publish (pull_request) Has been skipped
With the fast (no-think) route, gate-selection latency is now proportional to the ANSWER length (a no-think model runs ~10-13 tok/s), not hidden thinking. A verbose "reasoning" field pushed a selection to ~9s (over the 8s budget). Request compact JSON with a <=8-word reasoning → ~2s (vs ~9s prose, or ~1.2s with no reasoning). Validated live against vllm-fast (glm-4.6-reap-fast): 2.0s, valid selection JSON, zero reasoning tokens. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
| 3b96b0399e |
feat(gate): fast no-think selection + dedicated selection Llm (#88)
Some checks failed
|
|||
|
|
5225b54901 |
feat(gate): MCPCTL_GATE_SELECTION_LLM — pin a dedicated fast selection Llm
Some checks failed
CI/CD / lint (pull_request) Successful in 1m2s
CI/CD / typecheck (pull_request) Successful in 2m7s
CI/CD / test (pull_request) Successful in 1m19s
CI/CD / build (pull_request) Successful in 2m11s
CI/CD / smoke (pull_request) Failing after 2m47s
CI/CD / publish (pull_request) Has been skipped
Lets ops route gate prompt-selection at a fast (no-think) server Llm independent of the project's chat llmProvider — so selection stays ~1-2s while chat keeps its thinking model. Overrides the project llmProvider for the gate's server-selection path; unset → prior behavior (use the project's llmProvider). Pairs with the litellm qwen3-fast no-think alias (kubernetes-deployment) + a `vllm-fast` mcpd Llm. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
27480181bf |
feat(gate): ask reasoning models for a fast, no-think selection response
Prompt selection is mechanical classification — it gains nothing from chain-of- thought, and a reasoning model (qwen3-thinking) otherwise burns its whole budget reasoning (~40s, 6k+ chars) and blows the gate's 8s timeout. The gate's server selection request now includes thinking-suppression hints, forwarded verbatim by mcpd's passthrough adapter to litellm/vLLM: chat_template_kwargs.enable_thinking=false (Qwen3 hard-off) reasoning_effort=low (o-series / newer vLLM) Harmless for models that ignore them; if a backend rejects them the selector falls back to the local provider. Override via MCPCTL_GATE_SELECT_EXTRA_BODY (JSON), or '' to disable. NOT yet validated live — the qwen3-thinking vLLM is crashlooping again (0/1, HTTP 500), and whether litellm forwards chat_template_kwargs to vLLM is unconfirmed (the /no_think prompt directive was NOT honored). Validate when the backend recovers. mcpld gate/selector tests green (75). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
| 86942b1e91 |
feat(gate): route prompt-selection through mcpd server Llm (#87)
Some checks failed
|
|||
|
|
2729580974 |
feat(gate): route prompt-selection through mcpd's server Llm (credential tiering)
Some checks failed
CI/CD / typecheck (pull_request) Successful in 1m3s
CI/CD / lint (pull_request) Successful in 2m6s
CI/CD / test (pull_request) Successful in 1m18s
CI/CD / build (pull_request) Successful in 2m10s
CI/CD / smoke (pull_request) Failing after 3m9s
CI/CD / publish (pull_request) Has been skipped
Cloud/server keys belong at the k8s/mcpd level; mcplocal handles only the user's personal tokens. The gate's prompt-selection used mcplocal's LOCAL provider registry (heavy=anthropic = the user's OAuth token, which is API-gated and 404s), so selection always degraded. Now LlmPromptSelector tries sources in priority order: (1) the project's server Llm via mcpd's inference proxy (POST /api/v1/llms/:name/infer) — cloud/server keys stay at k8s — then (2) the local personal-token provider as fallback. First source that returns valid selection JSON wins. Extracted pickCompletionText (content ?? reasoning_content) + extractSelection helpers. Wiring: GatePluginConfig.llmProvider (from the project), threaded via createDefaultPlugin; handleBeginSession builds serverInfer from ctx.postToMcpd. Verified live: POST .../llms/vllm-current/infer returns valid selection JSON (qwen3-thinking, 4000-token budget). + selector tests (server preferred, local fallback on error, server-only). mcplocal suite green (755). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
| e75d2ba296 |
feat(proxy): favourite-index tool presentation + gate provider hardening (#86)
Some checks failed
|
|||
|
|
366b505c00 |
fix(providers): anthropic surfaces HTTP errors too (gate parity with openai)
Some checks failed
CI/CD / lint (pull_request) Successful in 1m6s
CI/CD / typecheck (pull_request) Successful in 1m5s
CI/CD / test (pull_request) Successful in 1m19s
CI/CD / build (pull_request) Successful in 2m14s
CI/CD / smoke (pull_request) Failing after 3m0s
CI/CD / publish (pull_request) Has been skipped
The local mcplocal gate resolves heavy=[anthropic]; its OAuth-gated key 404s the configured opus-4 model, and anthropic.ts (like openai.ts before this series) JSON.parsed the error body as a completion → empty content → the gate's misleading "did not contain valid selection JSON". Reject on status >= 400 with "Anthropic HTTP <status>: <body>" so the true reason (model-not-found / auth) surfaces as the degradedReason. + parity test. Note: this makes the error honest; it does not make the local gate SUCCEED — that needs a working heavy provider (the anthropic OAuth token is fundamentally gated). The qwen3-thinking gate path (k8s mcplocal) is addressed by the budget+reasoning fix in the previous commit. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
c9364a2f2a |
fix(favourite-index): route favourite/ calls even before tools/list (lazy resolver)
Some checks failed
CI/CD / typecheck (pull_request) Successful in 1m4s
CI/CD / lint (pull_request) Successful in 2m12s
CI/CD / test (pull_request) Successful in 1m20s
CI/CD / smoke (pull_request) Failing after 1m54s
CI/CD / build (pull_request) Successful in 4m31s
CI/CD / publish (pull_request) Has been skipped
The presented→canonical resolver was only built during tools/list, so a favourite/<tool> call arriving before any list this session fell through to the namespaced router and 404'd (all/ was fine — it prefix-strips). Real MCP clients list first, but the smoke test's initialize→call flow exposed it. Extract buildPresentation() and lazily rebuild the resolver in onToolCallBefore when a favourite//all/ name isn't resolved yet. + unit test (favourite/ call with no prior list) and realistic listTools() in the smoke routing case. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
9dbd2f175d |
fix(gate): stop masking backend errors + rescue thinking-model prompt selection
Some checks failed
CI/CD / lint (pull_request) Successful in 1m6s
CI/CD / typecheck (pull_request) Successful in 2m14s
CI/CD / test (pull_request) Successful in 1m22s
CI/CD / smoke (pull_request) Failing after 1m54s
CI/CD / build (pull_request) Successful in 4m17s
CI/CD / publish (pull_request) Has been skipped
Two independent, live-reproduced causes of "Smart prompt-selection unavailable
(LLM response did not contain valid selection JSON)" in the gate:
- Cause B (openai.ts request): the response handler JSON.parsed the body with no
res.statusCode check, so a LiteLLM/vLLM HTTP 500 ("Connection error") parsed as
an empty completion and surfaced as generic "bad JSON", masking backend-down.
Now reject on status >= 400 with "OpenAI HTTP <status>: <body>" so the selector
reports the true degradedReason.
- Cause A (parseResponse + selector budget): a reasoning model (qwen3-thinking)
spends its whole 1024-token budget in reasoning_content and emits content=null,
so the {selectedNames} JSON never appears. parseResponse now falls back to
reasoning_content (incl. provider_specific_fields) when content is empty, and
the selector budget is raised 1024 -> 4000 (env MCPCTL_GATE_SELECT_MAX_TOKENS)
so thinking + the JSON answer both fit.
Note: mcpd's chat path got a reasoning_content fallback in PR #67; this is the
separate mcplocal gate-selection provider, which never did. Tests: openai-provider
(500 rejects, reasoning_content capture/preference). mcplocal suite green (749).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
f614e9bb98 |
feat(proxy): favourite-index tool presentation (favourite/ + all/ + prefer instruction)
Some checks failed
CI/CD / lint (pull_request) Successful in 1m4s
CI/CD / test (pull_request) Successful in 1m23s
CI/CD / typecheck (pull_request) Successful in 2m36s
CI/CD / smoke (pull_request) Failing after 1m53s
CI/CD / build (pull_request) Successful in 4m16s
CI/CD / publish (pull_request) Has been skipped
Measured winner from the DGX-Spark bake-off (toolsim.py, 145-tool catalog): a
curated favourite/<tool> shortlist + the full all/<server>/<tool> catalog + a
load-bearing "prefer favourite/ first" instruction nearly halved wander (37→20)
and 2.5x'd first-pick (2→5/8) vs a flat catalog. The instruction is load-bearing;
enriching descriptions did not help.
- New mcplocal plugin `favourite-index.ts`: composes AFTER gate (no-ops while
gated), reshapes the ungated upstream catalog into favourite/ + all/, injects
the instruction (onInitialize), and rewrites presented names back to canonical
server/tool in onToolCallBefore so normal routing + content-pipeline still run.
Gate/agent virtual tools pass through untouched; favourites are upstream-only.
- compose.ts: onInitialize now concatenates plugin instructions (was first-non-null)
so favindex can contribute its banner alongside the gate's.
- Per-project config `Project.favouriteIndex` {enabled, tools[], maxFavourites};
surfaced to the proxy via discovery; wired at project-mcp-endpoint when enabled.
- Usage derivation: mcpd tool-usage ranking over tool_call_trace events
(normalizing presented names → canonical), GET /api/v1/audit/tool-usage, and
`mcpctl favourites suggest|list`.
- CLI: `create project` gains --favourite/--favourite-index/--max-favourites;
favouriteIndex round-trips through get -o yaml | apply -f. Completions regenerated.
- Tests: plugin unit (presentation, rewrite routing, gated no-op, collisions),
compose merge, canonicalizeToolName, buildFavouriteIndex, + a live smoke test.
- Docs: docs/tool-presentation.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
| 574fc63bb1 |
docs(reliability): fallback needs a real API key (#85)
Some checks failed
|
|||
|
|
88025d0ef5 |
docs(reliability): fallback target needs a real sk-ant-api03 key, not an OAuth token
Some checks failed
CI/CD / typecheck (pull_request) Successful in 1m14s
CI/CD / lint (pull_request) Successful in 2m15s
CI/CD / test (pull_request) Successful in 1m27s
CI/CD / smoke (pull_request) Failing after 2m47s
CI/CD / build (pull_request) Successful in 2m23s
CI/CD / publish (pull_request) Has been skipped
Record the empirical finding: a Claude subscription OAuth token is gated by Anthropic (404/429 as a raw API) and can't serve as the server-side fallback, even though mcpd's adapter now sends it via Bearer. The anthropic-fallback is wired-but-inert until mcpd Secret anthropic-key holds a real console API key. Also note the chain is now Pulumi-owned so pulumi up won't wipe it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
| e95aeeb3a2 |
fix(mcpd): anthropic adapter OAuth support (#84)
Some checks failed
|
|||
|
|
a1c545d21a |
fix(mcpd): anthropic adapter supports OAuth (sk-ant-oat) keys via Bearer
Some checks failed
CI/CD / lint (pull_request) Successful in 1m4s
CI/CD / test (pull_request) Successful in 1m19s
CI/CD / typecheck (pull_request) Successful in 2m11s
CI/CD / build (pull_request) Successful in 2m27s
CI/CD / smoke (pull_request) Failing after 2m48s
CI/CD / publish (pull_request) Has been skipped
Claude subscription / Claude Code keys are OAuth tokens and authenticate with
Authorization: Bearer, not x-api-key. mcpd's server adapter only sent x-api-key,
so registering an OAuth key as a fallback Llm 401'd ("invalid x-api-key").
Mirror the mcplocal client adapter's isOAuth switch so an OAuth key works as a
server-side anthropic fallback.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
| efbe99832b |
Merge pull request 'feat(chat): LLM failover chain + show which model answered' (#83) from feat/llm-failover-chain into main
Some checks failed
|
|||
|
|
18524c1c57 |
feat(chat): LLM failover chain + show which model answered
Some checks failed
CI/CD / typecheck (pull_request) Successful in 1m4s
CI/CD / lint (pull_request) Successful in 2m14s
CI/CD / test (pull_request) Successful in 1m18s
CI/CD / build (pull_request) Successful in 2m30s
CI/CD / smoke (pull_request) Failing after 3m18s
CI/CD / publish (pull_request) Has been skipped
Chat is LLM-essential but not model-specific — instead of failing when the
pinned model is down/drifted, it now fails over across an ordered chain and
reports which model actually answered.
- Ordered fallback: an Llm declares `extraConfig.fallbacks: string[]`; the
dispatcher builds primary-pool → fallback-pool(s) candidates and tries them
in order (resolveCandidatesWithFallbacks).
- Fail over on real failures, not just transport: runOneInference now advances
on a non-2xx status (e.g. a 400 from a drifted model) or an empty/invalid
completion, not only thrown transport errors. Streaming fails over
pre-first-chunk (already threw on 4xx).
- Transparency: ChatResult + the SSE `final` frame carry {llm, model,
failedOver}; the CLI prints `model: <llm> (<model>)` each turn and
`⚠ failed over → answered by …` when a fallback was used.
- Exhaustion names the last model + upstream body (not "no choice").
Tests: 3 failover unit tests (primary 400 → fallback answers + model reported;
primary answers → failedOver=false; all fail → clear aggregated error).
mcpd 948 + CLI 508 green; tsc + lint clean. docs/reliability.md updated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|