agentic-teams: epics, tasks, git forges, agent authority #139
Merged
michal
merged 36 commits from 2026-10-09 14:47:34 +00:00
feat/agentic-teams into main
36 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
527983d87e |
fix(db-tests): seed a SecretBackend for Secret model tests
Some checks failed
CI/CD / lint (pull_request) Successful in 1m17s
CI/CD / test (pull_request) Successful in 1m36s
CI/CD / typecheck (pull_request) Successful in 3m13s
CI/CD / smoke (pull_request) Failing after 2m4s
CI/CD / build (pull_request) Successful in 4m43s
CI/CD / publish (pull_request) Has been skipped
The four Secret tests in models.test.ts created rows via raw prisma.secret.create without a backendId, so the schema default '' violated Secret_backendId_fkey (that default only exists so 'db push' can add the column to pre-existing rows; real writes always carry a valid FK). Seed a plaintext backend per test, the same shape bootstrapSecretBackends creates, and clear SecretBackend rows in clearAllTables so nothing leaks between tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
24f202cb79 |
Merge origin/main into feat/agentic-teams
Conflict in src/mcpd/src/main.ts: both sides touched the import block. Kept this branch's mapUrlToPermission plus main's bootstrapUngatedGlobal; dropped main's SET_OWN_PASSWORD_OPERATION import — this branch moved that usage into middleware/url-permission.ts, which imports it itself. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
70bc7f1598 |
Merge origin/main into feat/agentic-teams
124 commits of main against 33 of agentic-teams. 22 files overlapped, 7 conflicted. Three needed real reconciliation rather than a side-pick: **The `volumes` collision.** Both branches independently added a `volumes` field to ContainerSpec meaning different things: main's is persistent storage keyed by a server-derived `claimName` that deliberately outlives an instance; mine is emptyDir scratch shared between an init container and the main container. Worse, each emitted the pod-level `volumes` list from its own helper, so spreading both would have silently kept only the last — a server with a PVC would have lost it the moment a session scratch volume appeared. Resolved by renaming the unreleased side: mine is now `scratchVolumes`, main's keeps `volumes` since it is already shipped and in use. `buildPodVolumes` now owns the whole pod-level list and emits both kinds; `buildPodExtras` is init containers only. fsGroup/securityContext stays tied to the PVC case, because an emptyDir is created writable by the container's own user and chowning it is work with no purpose. Five tests pin the coexistence so a later change cannot drop one kind again. **The extracted permission mapper.** I moved `mapUrlToPermission` out of main.ts into middleware/url-permission.ts so it could be tested; main kept it inline and kept editing it. Kept the extraction and ported main's one change across — the `/api/v1/audit/events/purge` mapping onto the existing `audit-purge` operation. Verified rule-by-rule that the extracted version is a superset: all 15 of main's rules plus the 5 agentic-teams ones. **The NUL byte in create.ts.** Both branches fixed the same defect independently, mine as `''` and main's as `'\0'`. Took main's. The rest were one-liners: .gitignore comment wording, and an add/add on proxy-long-running.test.ts where main's copy carries a progressive-delivery test mine lacked — took main's in full. Not changed, but worth flagging: main's generateDeploymentSpec hardcodes `automountServiceAccountToken: false` while generatePodSpec honours `spec.automountServiceAccountToken`. That asymmetry is main's own and predates this merge, so it is left exactly as-is rather than altered inside a merge commit. Verified: 3138 tests pass (237 files), build clean, prisma schema valid. Lint 809/80 vs 794/77 pre-merge — the delta is main's own debt; every file touched during conflict resolution was checked and introduces no new errors. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N4wNHWf7xSwnZCWpJcyv9p |
||
|
|
553834dbb1 |
docs(epics): document linking a task to its pull request
Accept only merges if the task carries a prNumber, and nothing said how a human sets one. It goes through the generic patch command; without this the merge gate reads as unreachable from the CLI. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N4wNHWf7xSwnZCWpJcyv9p |
||
|
|
6d38953a96 |
docs(agentic-teams): document the new resources, and make the webhook reachable
Five resources shipped with no documentation and one documented lie: docs/inference-tasks.md still told people `mcpctl get tasks` lists the inference queue. Work items own that name now. Adds docs/epics.md (the Epic -> EpicTask -> Task hierarchy, the state machine, risk-scaled review gates, the accept-merges gate, and reconciliation), docs/gitproviders.md (forge registration, discovery/import, merge-on-accept, post-merge reopen) and docs/agent-authority.md (the three run modes, and plainly which two are not implemented). Two things found while writing them, both fixed rather than filed: - `webhookSecretKey` was read by the webhook route but writable from NOWHERE -- absent from the create schema, update schema, service, CLI and apply spec. Since the endpoint fails closed on a null key, post-merge CI failures could never reopen their task; the board would keep claiming `merged` for work that broke the build. Adds --webhook-secret-key, the schema fields and the apply key, with tests covering persist / omit / explicit-null / reject-empty. Omission deliberately does not clear the column, so an unrelated edit cannot silently disarm the webhook. - src/cli/src/commands/create.ts contained a literal NUL byte inside a string literal (a binary-file check written with the raw character rather than an escape). Identical behaviour, but it made the whole file read as binary, so `grep -I` skipped it entirely and 1120 lines of CLI were invisible to every text search. Replaced with the escape. The Epic write path (criteria/status/tradeoffs) is API-only -- documented as such rather than papered over, and filed as task 180. Full suite 2612 green; lint at baseline 794/77. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N4wNHWf7xSwnZCWpJcyv9p |
||
|
|
2a85447fac |
feat(rbac): bound what an agent may DO by whoever invoked it
Closes 177. An agent carries a project's prompts and its MCP tools, and the only check on a chat turn was run:agents:<name> - so anyone who could talk to an agent inherited tool access to every project that agent had joined. Membership was the capability. AgentProject widened it from one pinned project to many. The rule now: knowledge travels with the agent, action is bounded by the authority of the run. Prompts and skills still load regardless of who is asking - a QA reviewer that forgets the project's conventions when a different person asks is not a reviewer. Every MCP tool call is gated. Authority is resolved once per run (services/run-authority.ts) and is per-RUN, not per-agent: the same agent answers a human under that human's authority and, once a scheduler exists, runs unattended under its own. `caller` intersects the agent's memberships with the caller's run:projects - reusing the exact grant the project-chat route already demands, rather than inventing a parallel permission. `agent` takes every membership without consulting a user. Both are implemented; `service-account` throws 501 rather than honouring a forgeable x-service-account header, and task 179 records what it needs first. Gated in BOTH listTools and callTool, deliberately: withholding a tool from the list is presentation, not enforcement - a model can invent a tool name it was never shown, and callTool is what would run it. The accepted consequence is stated plainly rather than papered over: a user can ask about a project they cannot reach and the agent will answer from its knowledge. The system block tells the model when it is holding such context so a persona can apply it without reciting it. That is guidance to a model, not a boundary - the boundary is that the tools are absent. Two sibling holes found while exploring and closed here: - Thread hijack. resolveThreadId on the agent path had no owner check, so anyone with run:agents:<name> could append to another user's thread by id and read its history back. The project path already did this correctly; mirrored, 404 not 403 so ids cannot be probed. - McpToken project escape. `project` travels in the request BODY, so the URL-based token scope check in main.ts never saw it - a token bound to project A could drive an agent into B's tools. The binding is now a ceiling. Tests: 19 authority tests (intersection, fail-closed with no user and with no RBAC wired, token ceiling, agent authority bypassing RBAC entirely, the 501) and 4 through the real chat path asserting prompts load while tools do not. Three existing assertions updated for the new dispatcher signature - a deliberate interface change, not tests bent to fit. Local: build clean, workspace 2608 passed (+16), lint unchanged at 794 errors. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N4wNHWf7xSwnZCWpJcyv9p |
||
|
|
f0ba6e8096 |
feat(git): reopen a task when CI fails on the commit its merge produced
Closes 151 — the last loop in the lifecycle. A task reaches `merged`, CI then fails on the merge commit, and the same task comes back as `reopened` instead of the breakage going unnoticed because the board already said done. Two properties make this safe rather than a liability. **It fails closed.** The endpoint is unauthenticated by nature — a forge calls it — so it accepts nothing unless the provider has a configured webhook signing secret AND the HMAC verifies. No secret, no effect. The signature is computed over the RAW body: re-serializing a parsed object changes the bytes, which is the classic way this check silently stops working, and there is a test for it. A 404 for an unknown provider is deliberately vague, because confirming which provider names exist is free reconnaissance on a public endpoint. **It matches precisely.** Accept now records `merge_commit_sha` — the commit the merge actually produced, which is what CI runs against — so a failing status finds exactly the task that caused it. The alternative, reopening "whatever merged most recently", would reopen innocent work. A failure on a commit mcpctl did not merge is somebody else's business and is ignored, as is a task that has already moved on from `merged`. Capturing the merge commit is best-effort by design: the merge already succeeded, so failing to re-read the sha must not fail the accept — it only costs the webhook its precise match later. The route is skipped by the RBAC hook, since the forge has no mcpctl identity. That is only safe because of the fail-closed rule above, and both the skip and the reason are asserted in tests. Tests: 12 covering every refusal path (no secret, unsigned, wrong key, signature over different bytes), the vague 404, the RBAC skip, and the reopen matrix. Local: build clean, workspace 2592 passed (+12), db 128 passed (same 4 pre-existing), lint unchanged at 794 errors. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N4wNHWf7xSwnZCWpJcyv9p |
||
|
|
ac9423e9c8 |
feat(git): merge the PR when a task is accepted
Closes 148/149 — the accept gate now lands code instead of just moving a row. Driver gains createPullRequest / getPullRequest / mergePullRequest, verified against the real Gitea API shapes rather than guessed: MergePullRequestOption requires a capitalised `Do`, and supports `head_commit_id`. That `head_commit_id` is the point. A human approved a SPECIFIC commit; if someone pushed after that, merging anyway lands code nobody reviewed. So the reviewed sha travels with the request and is checked twice — once by us for a clear MergeStaleError naming both commits, and again by the forge, which closes the race between our read and the merge. When it trips, the message says what happened and what to do: re-review the current head, then accept again. Accept merges BEFORE it transitions. If the forge refuses — conflicts, failing checks, a moved branch — the task stays in awaiting_review and no event is written. Reporting `merged` while main does not have the code is the single worst thing this gate could do, and there is a test asserting the transition never happens on a failed merge. Failure modes are answers, not stack traces: no PR attached is approval-only and succeeds; a repo with no forge connection explains that mcpctl cannot merge it; an already-merged PR is treated as success so a retry after a timeout is safe. owner/repo comes from `sourceFullName` when discovery recorded it, falling back to parsing https, ssh:// and scp-style clone URLs, and returning null rather than guessing when it cannot tell. The merger is injected and optional, so an mcpd without a git provider still runs and accept degrades to approval-only — which is also what the help text now says, restored after the earlier commit that removed a claim we could not back up. Tests: 7 driver PR tests (Do field, head pinning, stale refusal, idempotent re-merge, 405 → refusal), 10 merger tests incl. URL parsing, 7 accept-gate tests. Local: build clean, workspace 2580 passed (+23), lint unchanged at 794 errors. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N4wNHWf7xSwnZCWpJcyv9p |
||
|
|
821c45b2ec |
fix(cli): stop claiming that accept merges anything
`mcpctl tasks accept --help` said "fires the merge". Nothing merges. There is no createPullRequest or mergePullRequest anywhere in the codebase — the git-provider client is task 148 and is not built. Accept records human approval and moves the task to `merged`. Until 148/149 land, `merged` means "a human approved this", not "this is in main", and the help text and service comments now say so. A user who accepted a task expecting a PR to land would have been silently wrong. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
2a305f7045 |
feat(cli): describe formatters for the agentic-teams resources
Closes 167. The five new resources fell through to the generic key-value dump,
against the project rule that describe shows clean sections and tables. More to
the point, two things built earlier had no way to reach a human at all: the
roadmap endpoint had no client, and the Epic reconciliation was only visible as
raw JSON.
`describe epic` puts the delivery mismatch directly under the completeness
percentage it contradicts, because reading either one alone is the bug. Rendered
against a live mcpd:
Completeness: [....................] 0% (0/3)
...
Delivery: MISMATCH between what is claimed and what shipped
Tasks: 1 merged, 0 outstanding, 0 cancelled
Delivered, not claimed:
- wheels (1 merged, 0 outstanding)
No roadmap node owns it:
- engine, feel
plus the criteria checklist, accepted tradeoffs, and the roadmap with per-node
completeness and READY flags.
`describe task` derives the gates a task still owes from its risk and recorded
verdicts ("awaiting security_review, sre_review" vs "all passed"), and shows the
legal next transitions, PR linkage, verdicts and full history — so the answer to
"why won't this move" is on screen rather than requiring a failed attempt.
`describe repo` / `describe gitprovider` show the credential REFERENCE only.
The resolved value exists solely on the session-provision path and must never
reach a describe.
The roadmap, verdict and event fetches are all best-effort: an older mcpd 404s
and the resource still renders, rather than the whole describe failing over an
enrichment.
One bug found by looking at the output rather than the types: the Git section
opened on `!== undefined`, but JSON hands back null, so every task without a PR
printed an empty `Git:` header.
Local: build clean, workspace 2557 passed, lint unchanged at 794 errors.
Verified live against a local mcpd for epic, epictask and task.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N4wNHWf7xSwnZCWpJcyv9p
|
||
|
|
60db1be531 |
feat(epictask): make the criterion-id convention an invariant
Closes 175. Epic.doneCriteria and EpicTask.doneCriteria are both JSON blobs joined only by a free-form id, and nothing checked that a node's ids existed on its parent Epic. A typo produced a criterion belonging to nobody. The reconciliation added in 170 surfaces those as `unowned`, which is honest but late: the mistake was made at write time and is far cheaper to refuse there. Now create and update both reject unknown ids, naming every one of them and listing what the Epic actually has — or saying plainly that the Epic has no criteria yet, which is the likelier cause when a whole roadmap comes back empty. Enforced in the service, not the schema: production provisions with `prisma db push`, so a constraint would never exist where it matters. A node with no criteria of its own is still fine — not every roadmap node claims one — and an update that doesn't touch criteria doesn't pay for the check. This deliberately stops short of the EpicCriterion table with a real FK. That remains the stronger answer if criteria ever need to be addressed independently, but it costs a schema change plus migrating everything currently in the JSON blobs, and the invariant closes the actual failure mode today. Also files 177: agent chat --project reaches project prompts and MCP tools while checking only run:agents:<name>. Raised by the agent that built membership, which correctly refused to invent the policy mid-task. Pre-existing behaviour for a legacy pin, but membership widens the blast radius considerably — it should be a deliberate decision with a test. Local: build clean, workspace 2557 passed (+7), lint unchanged at 794 errors. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N4wNHWf7xSwnZCWpJcyv9p |
||
|
|
4515eb4517 |
fix: don't commit agent worktrees
`git add -A` swept .claude/worktrees/ into the previous commit as embedded git repositories. They are transient checkouts created for parallel agent runs and have no business in the tree — removed from the index and gitignored. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
a869f77070 |
merge: AgentProject membership — the junction is now load-bearing
Merges feat/agent-project-membership (task 166/152). An agent can finally join many projects: membership routes, per-membership role and optional brain override, chat resolving context from the membership, and CLI attach-agent / detach-agent / get memberships. The legacy Agent.projectId fallback is kept for this release as the schema comment promises. Conflict resolved in main.ts: that branch added its RBAC case to mapUrlToPermission inline, while this branch had extracted the function to middleware/url-permission.ts so it could be tested. Kept the extraction and ported the case into it — joining a team is `edit:agents:<name>`, not `create:agents`, which is the authority to mint a whole agent. Also fixes something the extraction makes possible. security.test.ts contained a hand-copied reproduction of mapUrlToPermission, because main.ts self-invokes main() and nothing in it was importable. A security test asserting against a copy stops testing the system the moment the real one changes, and this copy had already drifted. It now imports the real function; all 18 assertions still pass, against the code that actually runs. Local: build clean, workspace 2550 passed, lint unchanged at 794 errors. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N4wNHWf7xSwnZCWpJcyv9p |
||
|
|
1a28cc5098 |
feat(mcpd): make AgentProject real — an agent joins many projects
The AgentProject junction has existed since the data-model commit and nothing
read it. `grep -c agentProject src/mcpd/src/services/chat.service.ts` returned
0; chat resolved an agent's project from the legacy single `Agent.projectId`
scalar. So the schema claimed "an agent is a person who joins zero, one, or
many projects" (docs/agentic-teams.md §4) while the running system still gave
every agent exactly one project. A schema that lies about behaviour is worse
than a missing feature: it reads as done, so nobody builds it.
This makes the junction load-bearing.
Membership is now a real surface (task 152):
GET /api/v1/agents/:name/projects memberships
POST /api/v1/agents/:name/projects/:project join, body {role?, llm?}
DELETE /api/v1/agents/:name/projects/:project leave
GET /api/v1/projects/:name/memberships the roster, with roles
`role` is TEXT (developer|qa|security|sre in v1) and is validated in the
service layer, not the database — production provisions with `prisma db push`,
so a CHECK constraint written into a migration would never run there. Adding a
fifth role stays a one-line change.
RBAC: joining/leaving is `edit:agents:<name>`, not the `create:agents` the
default method→action mapping would have produced. Minting a whole new agent
and adding an existing one to a team are not the same authority; this mirrors
project↔server attach/detach already being `edit:projects`. Reading a roster is
`view:` on the side you asked from.
Chat honours it. `prepareContext` resolves the project from the membership when
the caller names one, and the resolution runs *before* the Llm lookup so a
per-membership `llmId` can replace the agent's pinned brain — and therefore its
whole failover pool — rather than patching the dispatch list afterwards. That
override is why the brain-ownership question (§15 Q1) can still go either way
without a migration: today the answer is per membership.
Naming a project the agent has not joined is an error that names the fix, not a
silent fallback. Answering from a *different* project's tools and prompts than
the one asked for is the failure mode worth being loud about.
The legacy scalar keeps working this release, as the schema comment promises:
`--project` accepts a membership *or* a legacy pin naming that same project,
bare `mcpctl chat <agent>` is unchanged, and `GET /projects/:p/agents` returns
the union of members and legacy-pinned agents so nothing disappears from a
project mid-migration (mcplocal's agents plugin reads that endpoint).
CLI: `mcpctl chat <agent> --project <p>` (previously rejected as "not both"),
plus `attach-agent` / `detach-agent` alongside the existing attach-server pair,
`get agents --project`, and `get memberships` with ROLE and BRAIN columns.
Completions come from the generator, not hand-edits.
Threads stay scoped to the agent, not to (agent, project) — §4 wants the
latter, but that needs a schema change and a story for what happens to those
threads when a membership is revoked. Deliberately not decided here.
Tests: 51 new (43 mcpd, 8 CLI). Full suite 2514 pass. Lint unchanged at
794 errors / 77 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
fc524583b1 |
chore(completions): regenerate after the epictask --criterion flag
The guard test caught it, which is its job — the generator derives flags from the commander program, so any new option makes the committed files stale. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
f2239852a1 |
feat(cli): --criterion on create epictask, and file the reconciliation follow-ups
Merges the Epic reconciliation (task 170) and closes the gap that made it inert from the terminal. `mcpctl create epictask` had no `--criterion` flag while `create epic` did, so a roadmap built from the CLI had no criteria on its nodes. The reconciliation attributes a criterion to a node by id, so every criterion would have reported as `unowned` and both mismatch lists would always have been empty — the feature would have looked like it worked and detected nothing. Files two follow-ups the reconciliation surfaced: - 175: criteria and roadmap nodes are linked only by a free-form id convention that nothing enforces. EpicTaskService never validates ids against the parent Epic, ids can repeat across nodes, and a node can carry an id the Epic never had. The reconciliation reports those as `unowned` rather than guessing an owner, which is right, but the convention should become an invariant before more is built on it. - 176: `get epic -o yaml` now emits computed `completeness`/`reconciliation`, which are not settable. apply tolerates them, but the round-trip rule says get output is apply input. Local: build clean, workspace 2499 passed, lint unchanged at 794 errors. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N4wNHWf7xSwnZCWpJcyv9p |
||
|
|
b6f3ff8a26 | Merge branch 'feat/epic-reconciliation' into feat/agentic-teams | ||
|
|
00373d2b27 |
feat(epics): reconcile claimed completeness against delivered work
An Epic could report 0% while every Task under it was merged. That is not a
cosmetic gap — it is exactly the blind spot the Epic exists to remove
(docs/agentic-teams.md §1b: "we completed all the tasks and got a stick with
wheels instead of a car"). An Epic that cannot see its own delivered work
cannot detect that, because `completeness()` reads only `doneCriteria[].met`
and nothing flips those except a human hitting the criteria endpoint.
So read the same question a second time, from the delivery side, and report
where the two readings disagree — in both directions:
- deliveredButUnclaimed — unmet criteria whose owning roadmap node has
merged work. Somebody delivered and nobody ticked the box.
- claimedButUndelivered — criteria marked met with nothing merged
underneath. The box is ticked and nothing shipped. The dangerous
direction, and the one that is easy to forget to look for.
- unowned — criteria no roadmap node claims at all. They cannot appear in
either list above, so without a bucket of their own they would vanish
inside a clean-looking reconciliation.
Nothing is written. `met` stays exactly as a human or agent set it: deriving
it from task state would assert that finishing the tasks means delivering the
vision, which is the assumption under audit. The mismatch is reported, not
silently resolved.
Attribution is by criterion id, because that is the only link the schema
offers — EpicTask.doneCriteria is documented as "the decomposed subset of the
Epic's doneCriteria this node owns" and carries the same ids, but nothing
enforces it. Rather than guess an owner by name similarity or ordering, the
convention is treated as a claim and its failures are surfaced as `unowned`.
A foreign key from criterion to epictask would end the ambiguity; this reports
the ambiguity instead of hiding it.
Judgement calls, made explicit so they can be argued with:
- `merged` alone counts as delivered. `done` only leaves the developer's
loop and `awaiting_review` still needs a human; `reopened` means a merge
did not hold, so it counts as outstanding.
- `cancelled` is neither delivered nor outstanding. Counting it as owed
would leave a node permanently undelivered because someone abandoned a
task; counting it as delivered is a lie. It is reported on its own.
- a node with no tasks is undelivered, not exempt — a met criterion over an
undecomposed node is still a claim with nothing behind it. The empty task
rollup is what distinguishes it from "built but not merged".
- an epictask's own `status` is another claim, not evidence, so it is
carried for context and never counted as delivery.
Surfaced on GET /api/v1/epics/:id next to the completeness it contradicts —
a mismatch is only useful where the misleading percentage is already on
screen — and standalone at GET /api/v1/epics/:id/reconciliation for callers
that do not want a 200KB Epic body with it. Deliberately kept off the list
route, which would pay a query per roadmap node per Epic.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
1220b974dc |
fix(rbac): correct the non-CRUD verbs, and make the mapping testable
Closes 134/164. The five agentic-teams resources had RBAC slots but the
non-CRUD verbs had no cases, so they fell through to the method default and
every one of them read as `create:<resource>`. That is wrong in both
directions: it let anyone who may CREATE a task also ACCEPT one — the human gate
that fires a merge — and it refused a reviewer who may drive tasks but not
create them.
Corrected, following the verb the codebase already uses for "use this resource
to do work" (llms/infer, agents/chat, projects/chat):
- POST /tasks/:id/{transition,accept,reject,verdicts} → run:tasks:<name>
- {GET,POST} /gitproviders/:id/{repos,import,probe} → run:gitproviders:<name>.
Discovery calls OUT to the forge with its stored token, so holding
view:gitproviders should let you see that a forge is registered, not use its
credential.
- POST /epics/:id/{status,tradeoffs,criteria} → edit:epics:<name>. These rewrite
the vision record.
- GET /epics/:id/roadmap → view:epics:<name>
- {POST,DELETE} /epictasks/:id/edges → edit:epictasks:<name>
mapUrlToPermission moved from main.ts to middleware/url-permission.ts. main.ts
calls main() at module load, so nothing in it is importable from a test — an
authorization mapping that cannot be tested is an authorization mapping nobody
has checked, which is exactly how these verbs shipped mis-scoped.
15 tests now assert the mapping rather than the mere presence of the strings,
including that a bare create grant does NOT cover the gated verbs, that work
items and the inference queue never resolve to the same resource for any method,
and that the pre-existing mappings (llm infer, agent chat, operations, the
personalities/skills piggybacks) still hold.
Local: build clean, workspace 2478 passed (+15), lint back to the repo's
baseline 794 errors after making the five new match guards explicit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N4wNHWf7xSwnZCWpJcyv9p
|
||
|
|
d9535bc51f |
chore(taskmaster): file 174 — scope project-only commands out of root --help
The tasks/repos command groups added with the CLI work sit at the root of `mcpctl --help`, but both only function inside a project context. Beyond the clutter, it teaches the wrong data model: a task reads as a peer of servers and secrets rather than a leaf of the epic tree. Convention recorded as the `scope-commands-to-their-context` prompt in the mcpctl-development project. Note `attach-server`/`detach-server` are already hidden, so the pattern exists and was simply not applied consistently. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
5f5efd1144 |
fix(backup): cover the agentic-teams resources — closes the data-loss gap
The audit found that none of the new resources were in BACKUP_KINDS, so
git-backup never captured them and a restore would lose every epic, roadmap
node, repo and forge registration. Closes 135/165.
Backed up: gitprovider, repo, epic, epictask — configuration and the vision
layer. NOT tasks: a work item carries in-flight state (lifecycle position,
verdicts, PR links) and restoring a stale one from git would resurrect work that
has since moved on. That is the same reasoning that keeps McpInstance out of the
backup, and there is a test asserting it stays out.
Two properties the tests pin, because both are the kind of thing that fails
silently:
- **Credentials serialize as a REFERENCE, never a value.** The backup repo is a
plaintext git checkout; emitting a resolved token would export every forge
credential to disk in cleartext. credSecretId/credSecretKey are internal
fields and become `credRef: {name, key}`; restore re-resolves against whatever
the secret backend holds now.
- **Relations serialize by name, not foreign key.** A restore into a different
database has different cuids, so a backed-up projectId would dangle.
Restore ordering follows dependencies: a forge before the repos that reference
it, an epic before its roadmap nodes.
Two deliberate refusals on the restore path: an Epic's `status` is never written
by a restore, so a stale backup cannot silently mark it delivered — that
terminal state is only reachable through setStatus and its rules; and a
GitProvider is not probed on restore, because the forge may legitimately be
unreachable while a backup replays and failing the whole restore for that is
worse than a row that needs a later `probe`.
The pre-existing APPLY_ORDER length guard caught this change, which is exactly
what it is for; updated 9 → 13 and tied it to BACKUP_KINDS.length so the two
cannot drift apart again.
Also marks 153 done — `mcpctl tasks accept/reject` shipped with the CLI work; the
generated graph had it blocked behind the git-provider merge, but the gate itself
does not need it.
Local: build clean, workspace 2463 passed (+6), lint unchanged at 794 errors.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N4wNHWf7xSwnZCWpJcyv9p
|
||
|
|
300c42b404 |
fix(backup): restore skills on git restore, and record the audited gaps
Found while auditing what this branch is missing: skills are serialized into the backup repo and enqueued on every mutation, but `importResource` and `deleteResource` in main.ts have no `skill` case — so a git restore silently drops every skill. SkillService has had upsertByName/deleteByName the whole time; the switch arms were simply never added. Pre-existing, not introduced here. Also records ten tasks (164-173) for gaps found by auditing the branch against the codebase rather than against the plan: - 164 RBAC enforcement tests. Five resources and a namespace split shipped with no test proving the gating works. Also flags that the non-CRUD verbs (transition/accept/reject, import/probe) currently fall through to the default method mapping and are probably mis-scoped. - 165 Backup coverage for the agentic-teams resources — same class of silent data loss as the skill bug above, for five more kinds. - 166 AgentProject is inert: chat still resolves context from the legacy Agent.projectId scalar, so "an agent joins many projects" — a core idea of the design — is not actually usable yet. - 167 describe formatters (currently a generic key-value dump, against the project rule) including a client for the roadmap endpoint, which has no caller. - 168 Docs, including the breaking `mcpctl get tasks` rename that exists only in a commit message today. - 169 Smoke tests for the forge flow. - 170 Epic completeness ignores delivered work: an Epic can read 0% while every task under it is merged, which is precisely the blind spot the Epic exists to remove. - 171 `mcpctl chat mcpctl` has never been run end to end — the tool layer is verified, the LLM-picks-a-tool loop and the mcplocal token hop are not. - 172 Nothing on this branch is live; everything was verified against a local mcpd on the test database. - 173 Web UI + Pulumi kinds (cosmetic). Local: build clean, workspace 2457 passed, lint unchanged at 794 errors. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N4wNHWf7xSwnZCWpJcyv9p |
||
|
|
53507e876a |
feat(cli): apply -f support for agentic-teams resources, plus vertical smoke tests
Closes two stated project rules this work had been violating. **`get -o yaml` must round-trip through `apply -f`.** The five new resources were creatable and listable but absent from apply, so what `get` emitted was not valid apply input. They are now applied in dependency order — gitproviders → repos → epics → epictasks → tasks — since each references the previous by name, and each block is independently fault-tolerant so one bad entry does not abort the rest of a manifest. `edit` accepts them too. `state` is deliberately NOT in the task spec. Lifecycle moves go through `mcpctl tasks advance`, which enforces the transition table and the risk gate; a manifest that could set state would route straight around both. epictask `dependsOn` edges are re-applied on every run, which is safe because the server treats a duplicate edge as a no-op. **Every feature ships smoke tests.** Adds the vertical: create an epic with mixed outcome/quality criteria, round-trip it through get -o yaml → apply -f, chain an epictask, then drive a task through the lifecycle asserting the risk gate blocks `done` without a passing QA verdict, that an illegal transition is refused, that history carries every transition and the verdict, and that reject without a comment fails. Skips when mcpd is unreachable; a failure against a reachable daemon means it predates the feature. The round-trip assertion is the interesting one: it feeds `get`'s own output straight back into `apply` and fails if the two surfaces have drifted, rather than trusting that they agree. Local: build clean, workspace 2457 passed, db 128 passed (same 4 pre-existing Secret failures), lint back to the repo's baseline 794 errors after replacing destructured discards with an explicit omit() helper. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N4wNHWf7xSwnZCWpJcyv9p |
||
|
|
77c45a2715 |
feat(mcpd): mcpctl system agent — operates mcpctl with the caller's permissions
A system agent, seeded like the system project and system skills, whose entire
job is running mcpctl for the user:
mcpctl chat mcpctl -m 'Make a prompt in the SRE project saying we run multiple
agents on the same source at once, so always use git worktrees'
## The permission model is the design
Every tool call loops back through mcpd's own HTTP API carrying the CALLING
USER's bearer token. The agent holds no credential of its own and never touches
the service layer directly, so the existing auth hook, RBAC hook, zod
validation, ownership scoping and audit trail all apply verbatim. It is exactly
as privileged as the person talking to it, never more.
Calling services directly would have been simpler and would have meant
re-implementing every one of those checks at a second call site — precisely the
RBAC carve-out this project refuses to make. A 403 is surfaced as a 403, not
retried or routed around; the system prompt tells the agent to report it.
Verified live, not just mocked: the same tool with an admin token created a
project, and with a plain user's token returned "Permission denied (403)".
No token at all refuses before any request is made; a bad token is rejected by
mcpd's own auth, because there is no ambient credential to fall back on.
## Shape
- `Agent.toolset` (TEXT, default 'project') selects the surface. 'mcpctl' means
no project prompts and no MCP tools — managing mcpctl is the whole job, and an
unrelated project's context would be noise it might act on.
- Five generic tools (list/get/create/update/delete + resource) rather than one
per resource. mcpctl is a REST API over named resources, so that is the honest
surface, and this repo's own tool-selection findings say a scoped catalogue
beats a large flat one.
- Names are resolved to ids by listing first, and a miss reports what IS
available, so the model never guesses an id.
- Resource is a closed enum: the model cannot invent an endpoint.
Also exercised end to end against a live mcpd: created the worktrees prompt in
the `sre` project, listed it back, and read its content — the user's original
example, working.
Tests: 17 unit tests covering the token plumbing, the no-token and 403 paths,
resource allowlisting, name resolution, and request shaping.
Local: build clean, workspace 2457 passed (+17), lint unchanged at 794 errors.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N4wNHWf7xSwnZCWpJcyv9p
|
||
|
|
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 |
||
|
|
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 |
||
|
|
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
|
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |