Releases: benjaminLedel/covey-plugin-pack
Release list
v0.14.0 — the checkout repairs
Three faults in gitlab/checkout and one in gitlab/read_file, all found by a QA agent that set up a Laravel application from an 885 MB project and spent about fifteen turns on the workshop rather than on the work (#1). Plus a settable browser viewport (#2).
The subtree landed inside itself. With ?path=stupla/public GitLab's archive carries the full repository path, and the extractor stripped only the shell directory (projname-ref-sha) before unpacking into the subdirectory — so the files ended up under stupla/public/stupla/public/. It was noticed because public/index.php was missing, after two fetches of 5,703 files each. Paths are now normalised to repository-relative whichever shape the server sends, and that is also what makes several partial checkouts grow into one tree, which was the intent all along. The old test did not catch it: its fixture archive was already trimmed — it tested the case the server does not send.
A partial checkout brought no build files. No artisan, no composer.json, no composer.lock — a PHP project cannot be built from that, and you find out at the first composer install. The agent's way out is the part nobody should have to write down: it called the full checkout knowing it would fail at the size limit, because the aborted fetch left the root files behind. The blobs in the directories above the subtree now come along, bounded per level and per file: build files, not a second way to pull a whole repository.
A tree in daily use looked like the oldest one. Eviction measures the mtime of the top directory, and a tree assembled from thirteen partial checkouts gets its files written inside it — the top never moves. It was as old as its first fetch while being the only one worked in, and five unrelated checkouts pushed it out. The root is touched on every checkout now, and what was evicted comes back as a field rather than as a sentence inside a long hint: a list does not get lost in a paragraph.
read_file can fetch the rest. The cut at 512 kB was reported properly ("truncated": true) and was still a trap: there was no way to get the remainder. composer.lock at 598,917 bytes arrived unusable, and whoever does not check the field builds with half a lock file — a state nobody can reproduce afterwards. read_file takes an offset now and says where the next piece starts, read through a Range header; a server that ignores Range answers 200 with the whole file, and the beginning is dropped rather than handed back twice.
The browser window size stops being a constant. Chrome started at a fixed 1440x900 and nothing changed it, so every page an agent ever saw was rendered at one width — responsive behaviour was not testable at all, and a screenshot filed against a ticket did not say what width it was taken at. There is a viewport action now, with named sizes (phone, tablet, desktop, wide) beside raw numbers, going through emulation rather than the window. screenshot takes the same parameters and puts the session back afterwards.
Requires SDK v0.4.0, which keeps every working copy used within the last half hour beyond COVEY_CHECKOUT_KEEP — that is what stops five checkouts passing through from removing the sixth.
v0.13.0 — the two Jira repairs a hand-off died on
Two repairs, both measured on a real instance, both of the kind that fail quietly.
assign takes the name that stands on the ticket. Cloud wants an accountId in the assignee field and answers anything else with a 404 that reads like a permission problem — "the specified user does not exist or you do not have permission". An agent has the name from the ticket, not the id, so seven assignments in a row failed on covey.work and the hand-off back to the reporter never happened. The name is resolved before the PUT: the assignable search first, because that list is narrowed to the people who may actually hold this issue, the site-wide search as the fallback where the endpoint is not offered. An accountId is passed straight through — the lookup is for names, not a toll on every call. An ambiguous name is an error naming the candidates: assigning the wrong person is quiet, and the agent is the last one to notice.
An id that is a number in the response and a string in the docs. GET /rest/api/3/attachment/{id} answers with "id": 248819 while the same attachment inside the issue carries "id": "248819". The struct insisted on the documented shape, so the whole response failed to decode and download_attachment died on cannot unmarshal number into Go struct field rawAttachment.id. The agent then reported that it could not see the screenshot and guessed a proxy problem, which was not what happened. flexString reads both and keeps the string, applied to the ids that come off the wire (attachment, comment, transition).
Beside it: a media node without an alt text carried only the media id, which is not the attachment id download_attachment takes. It now points at list_attachments instead of inviting a call that cannot work.
v0.12.0 — Confluence, the third system on the desk
Jira holds the ticket, GitLab holds the code, and neither holds what they are supposed to mean. The specification a ticket links to, the runbook a change invalidates, the release note somebody looks for next quarter — an agent could read none of it and write none of it back.
Compiled, and for a harder reason than Jira's. A page is stored as an XHTML derivative with Atlassian's own elements woven through it: <ac:structured-macro> for a code block, <ac:link> around an <ri:page> for a link, <ac:image> around an <ri:attachment> for a picture. Reading it raw would put several times the page's own text into the agent's context, spread across attributes and CDATA; writing it raw produces almost-XHTML and a 400 nobody can learn from. So storage.go renders it to Markdown and markdown.go builds it back, and the agent writes what it writes anyway.
One trap in the parser is worth naming, because it cost an afternoon: xml.HTMLAutoClose contains link, the decoder matches by local name alone, and <ac:link> therefore closes the moment it opens — the rest of the page arrives as a syntax error. The void-element list is written out here without it.
Two deployments as with Jira, and further apart: Cloud moved pages to v2 but left search on v1, and both sit under a /wiki context path that does not exist on Server/Data Center. The token's shape decides, the path is appended when somebody leaves it out, and every method that branches says so where it branches.
Appending is the action the doc steers towards. Almost everything an agent writes to a wiki is an addition, and replacing a page can delete what a person wrote; they are two guard-rail subjects for that reason. Both take the version the agent read — Confluence numbers revisions, and that number is what turns "I write my change" into "I write my change unless somebody wrote first". Without it the last write wins and the result says so.
No heartbeat gate and no webhook, on purpose: nobody is assigned a page, and Confluence Cloud has no webhook an admin can simply enter. A test asserts both, so the setup doc cannot quietly become a lie.
v0.11.0 — Jira, the ticket half of a developer's day
A developer agent had a repository and no board. GitLab and GitHub carry issues of their own, but the company that plans in Jira and codes in GitLab had no way to hand its agent the ticket it is actually working on.
Why this is a compiled plugin and not a manifest. Three things decide it. Every long text in Jira Cloud is a document tree, not a string: an ADF description is roughly ten times the size of the sentence it carries and it stands in the agent's context in full, while an agent asked to write one produces almost-ADF and gets a 400 it cannot learn from — so adf.go translates both ways and the agent writes Markdown, which is what it writes anyway. A status in Jira is not set but reached, through a transition whose id differs per workflow, so transition takes the name of the transition or of the target status and resolves it, and when it cannot it says what the workflow does offer instead of failing blind. And a screenshot on a bug report is looked at, not guessed at: download_attachment brokers the file into the sandbox for the Read tool.
Cloud and Server/Data Center are one plugin. The shape of the token decides which is spoken to — a pair with a colon is Cloud, a single value a personal access token — and with it the API version, the search endpoint (Cloud has retired /search in favour of /search/jql), the assignee field and the format of every body. auth= and api= write the inference out where it is wrong.
The wall around a pinned project is the same idea as Salesforce's queue, only cheaper: a Jira key carries its project in front of the hyphen, so no call is needed to know where ACME-17 belongs. A search is narrowed by bracketing the agent's own query — appended behind an OR the condition would bind to the last term only, which is a wall with a hole exactly where somebody used an OR.
Intake both ways: a heartbeat gate with three sub-scopes (assigned, unassigned, both) whose signature is built from the updated timestamps, so a ticket the agent looked at and parked does not wake it again; and the webhook, where a comment is work, an assignment is work, and everything else only wakes a task that was blocked on that issue.
The prompt doc carries the loop that makes the two systems one workflow: take the ticket on before starting, name the branch after the key, begin every commit message with it — that prefix is what makes the branch, the commits and the merge request appear on the ticket — then comment the MR link and move the status on.
Also in this release: the setup doc now names COVEY_<SYSTEM>_WEBHOOK_SECRET, the counterpart Covey reads — an instruction that stopped one step short produced a signed webhook nobody verified. And the CI security jobs move with the toolchain (govulncheck v1.7.0, gosec v2.28.0): the older pinned versions met Go 1.27 syntax they did not know and panicked, which in a security job reads like a broken build.
v0.10.0 — the pinned queue becomes a wall
v0.9.0 put the queue into the credential and made it the default for list_cases. That was half a boundary and therefore none: every other action addresses a case by id or by the number a customer quotes, and a number is something an agent can simply be handed. Narrowing the listing only hid the other cases; it did not put them out of reach.
The check moves to where cases are addressed — one place in Execute, before the switch. Is this credential pinned to a queue and does this call name a case, then the case is read once and its owner checked. Everything that follows inherits it: get_case, list_messages, reply, set_status, escalate, list_files, attach_file, including the ones added later. get_case reuses that read, so the wall costs no extra call for the action that would have made the same one anyway.
Three things to know before upgrading:
search_casesis the only action without a WHERE clause. SOSL cannot filter on the polymorphic owner, so the result is narrowed after the fact. Without it, search would have been the hole.- Naming a different queue in the call is now an error. Under a ceiling that is not a narrower request but a wider one, and the point of putting the queue in the credential is that the agent cannot widen its own reach.
assignedno longer beats the pinned queue, it fails against it. A case owned by a queue is not owned by a user, so the two can never both hold; the previous "assigned wins" was the ceiling leaking.
The error names the actual owner rather than hiding it. Whoever hits this wall usually has the wrong case number, and "belongs to somebody else" without saying whom costs a second round trip to find out.
v0.9.0 — an agent's queue belongs to the agent
There was one way to hold Salesforce cases to a single queue: COVEY_SALESFORCE_INTAKE_QUEUES, a process environment variable. That is the wrong scope for the thing it configures. Which queue somebody looks after is a property of the employee — two agents on one installation are the normal case, and an env var narrows both or neither.
The queue moves into the credential, beside the other per-agent components of salesforce_url:
salesforce_url = https://acme.my.salesforce.com queue="Support Tier 1"
The quotes are the reason splitComponents replaces strings.Fields: real queue names have spaces in them ("Digital Learning Support"), and a setting that cannot hold the value people actually have is a setting nobody uses. An unquoted name with a space now fails at the credential rather than silently becoming a queue called "Digital".
v0.8.0 — the queues have names
Two of this plugin's settings are configured by name — COVEY_SALESFORCE_INTAKE_QUEUES and COVEY_SALESFORCE_ESCALATION_QUEUE — and until now the only place to read those names was the Salesforce setup UI. That is precisely where whoever operates the platform is not, so the name got copied out of a screenshot, with whatever whitespace came along.
list_queues answers it from the platform: the case queues of the org, by name, sorted, with the developer name and the queue mail beside them. The query goes through QueueSobject rather than Group, so what comes back are the queues that may actually own a case, and a queue that carries several object types appears once rather than once per type.
Each entry also carries in_intake_scope. Listing which queues exist is only half the question; the one somebody actually has is "why do the cases from this queue never reach me", and that is the allowlist, not the org.
The read is its own guard-rail subject (salesforce:list_queues), so an agent held to reading is not held back by it.
Beside it: the CI registration check now filters on what a package is (go list -f '{{if ne .Name "main"}}…') rather than on where it sits. The wasm modules from v0.7.0 broke it, and the message said why — a program has no import path anybody can use and registers with nothing in that process.
v0.7.0 — three plugins become wasm modules
Three plugins that were compiled into every Covey binary are now WebAssembly modules, installable from the catalogue.
| Module | Was held in the binary by | Cleared by |
|---|---|---|
zammad |
the webhook — HMAC, dedup, correlation and the wake decision are judgement, not field lookups | the webhook op |
vulndb |
scan_lockfile reads the lock file out of the agent's checkout |
the read_file request |
k8s |
the cluster CA, with nowhere to put it | target.Credential.CA, brokered from k8s_ca |
They need Covey 0.6.0 or newer — the capabilities above are not in 0.5.0.
Two things got smaller, and both are worth reading before upgrading.
vulndb_token no longer has an effect. A module is never handed a credential for a host it merely declared, and all six of vulndb's sources are declared, so NVD answers at its anonymous rate limit (5 requests per 30 seconds instead of 50). The advisory action survives it the way it survives any source that does not answer — a note beside the merged result.
k8s no longer takes ca_pem as an action parameter. Store the certificate as the secret k8s_ca and the host builds the trust store from it. This is a fix as much as a move: the certificate used to travel through the model's context, the guard-rail subject and the recording of every single call.
COVEY_ZAMMAD_REPLY_TYPE is now the optional reply_type parameter on reply. COVEY_ZAMMAD_INTAKE_GROUPS is gone — a Zammad trigger has had a condition on the group all along, and filtering there means the request is never made.
Verifying these artefacts. Each is built from this tag with -trimpath and the toolchain go.mod names (1.26.6), and the catalogue pins the digest below. The index rebuilds them from source on every pull request and refuses the entry unless the bytes match, so "we reviewed the code" says something about the binary you install.
f7370c888a08315cea14dedbb580aaea784950ab5d0d4c0936ea93dc995ceee3 zammad.wasm
85a9a6400bf6527bf9b9f428e1dc83d718d03863352122a9a5855f57215a6d4c vulndb.wasm
dfd6fc580d4d8ec27de65bfbc05c270d1277a79bbf7a379838bc0e8a11c48041 k8s.wasm