Repository navigation
MarIA: permissions, agents, worktrees, questions, @-agent chains and Ruflo memory - #2
Conversation
Fail fast in the worker with an actionable message instead of the supabase-js realtime WebSocket error, add .nvmrc and bump engines. Co-Authored-By: RuFlo <ruv@ruv.net> Claude-Session: https://claude.ai/code/session_01EYLzTmzezSkXAERkr41FBp
…enials - Append a system prompt telling the agent it runs unattended: no cd or chained commands (so pre-approved tool patterns match), no retrying denied variants, end with a question for follow-ups. - Show the result event's permission_denials in the activity feed so the user knows which tools to add to MARIA_ALLOWED_TOOLS. - Broaden the example allowlist to common npm/node/git read commands. Co-Authored-By: RuFlo <ruv@ruv.net> Claude-Session: https://claude.ai/code/session_01EYLzTmzezSkXAERkr41FBp
…er start Co-Authored-By: RuFlo <ruv@ruv.net> Claude-Session: https://claude.ai/code/session_01EYLzTmzezSkXAERkr41FBp
Those variants miss simple allowlist patterns like Bash(git status) and get denied; ask for the plain form run from the working directory. Co-Authored-By: RuFlo <ruv@ruv.net> Claude-Session: https://claude.ai/code/session_01EYLzTmzezSkXAERkr41FBp
Instead of auto-denying tools that are not pre-approved, the agent's permission prompts are now routed to the user: - worker/permission-mcp.ts: a minimal stdio MCP server that Claude Code calls through --permission-prompt-tool. It records each request in maria.permission_requests and waits for the user's decision (fails closed: deny on timeout or technical error). - Migration 0002: permission_requests table, decide_permission() RPC (members only, first decision wins), realtime publication. - Front: PermissionPrompt modal with tool, command summary and full parameters; tab title shows pending count. - Pending requests expire when a mission ends or the worker restarts. - Configurable via MARIA_INTERACTIVE_PERMISSIONS (default on) and MARIA_PERMISSION_TIMEOUT_MS (default 10 min). Verified with the real claude CLI (allow runs the command, deny returns the refusal to the agent) and the SQL against Postgres 16. Co-Authored-By: RuFlo <ruv@ruv.net> Claude-Session: https://claude.ai/code/session_01EYLzTmzezSkXAERkr41FBp
…denying - Read mcp_servers from the init event; if the 'maria' permission server is not connected, post an error in the activity feed explaining that non-allowlisted actions will be denied without a prompt. - Log permission-server errors (startup and Supabase failures) to $TMPDIR/maria-permissions.log, since Claude Code hides MCP stderr. Co-Authored-By: RuFlo <ruv@ruv.net> Claude-Session: https://claude.ai/code/session_01EYLzTmzezSkXAERkr41FBp
- src/lib/agents.ts rebuilds the mission's agents from the event stream: each Agent/Task tool call opens a sub-agent, messages carrying its parent_tool_use_id count as its actions, and the matching tool_result closes it (done / error; interrupted if the mission ends first). Also tallies calls to Ruflo MCP tools (swarm_init, agent_spawn...). - AgentsPanel shows main agent, sub-agents (type, description, status, action count, last action, nesting) and the Ruflo section. - After a mission that used Ruflo, the worker snapshots the registry via `ruflo agent list --format json` into missions.ruflo_agents (migration 0003). `--all` is avoided: the CLI turns it into a status === 'all' filter that never matches. Command configurable via MARIA_RUFLO_CMD. Verified against a real stream-json capture of a sub-agent run and the real ruflo CLI (spawned agents are listed). Co-Authored-By: RuFlo <ruv@ruv.net> Claude-Session: https://claude.ai/code/session_01EYLzTmzezSkXAERkr41FBp
…ished - Background sub-agents return 'Async agent launched' immediately; the panel no longer marks them done on that result. The final status now comes from the system task_notification event (completed / failed / stopped), and task_progress keeps the action count up to date. - Feed: one line per finished sub-agent, and intermediate result events (one per turn with async agents) are labelled 'Tour terminé' so only the last one reads as the end of the mission. Co-Authored-By: RuFlo <ruv@ruv.net> Claude-Session: https://claude.ai/code/session_01EYLzTmzezSkXAERkr41FBp
…claude/zen-hawking-ioda4g
- New per-mission option "Branche isolée": the worker creates a worktree on branch maria/<id>-<slug> from the workspace HEAD, links shared untracked state (.claude-flow, .swarm, node_modules) and copies local config (.mcp.json, .claude, CLAUDE.md) when they are not versioned. - Worktree missions run in parallel (MARIA_MAX_PARALLEL per workspace, default 3); in-place missions and merges stay exclusive on the main folder. claim_next_mission now takes in-place and worktree eligible workspaces separately (migration 0004). - At the end of a mission, the agent's changes are committed on the branch (linked/copied entries excluded). "Continuer" reuses the parent's worktree so the Claude session can resume in the same directory. - Branch panel in the mission view: merge into the workspace's current branch (refused if it has uncommitted changes; aborted cleanly on conflict) or discard (worktree and branch removed), via the request_worktree_action RPC; the worker executes and reports errors. Verified on real git (create, commit, merge, dirty refusal, conflict abort, discard), the SQL on Postgres 16, and two parallel missions through the runner with a fake claude. Co-Authored-By: RuFlo <ruv@ruv.net> Claude-Session: https://claude.ai/code/session_01EYLzTmzezSkXAERkr41FBp
With no terminal the interactive question is lost ('The user did not
answer'). Disallow the tool so the agent asks in plain text at the end
of its answer; the user replies with 'Continuer'.
Co-Authored-By: RuFlo <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_01EYLzTmzezSkXAERkr41FBp
AskUserQuestion already reaches MarIA's permission prompt tool; it used to
be approved without answers, so the agent got "did not answer".
- The permission MCP server now returns the user's answers as
updatedInput.answers ({ question: answer }), the format Claude Code
expects; skipping or timing out tells the agent to proceed with
reasonable choices and say so.
- Migration 0005: permission_requests.response + answer_question() RPC
(members only, AskUserQuestion requests only, first answer wins).
- Front: the pop-up shows each question with its options (single or
multiple choice), an "Autre" free-text answer, "Répondre" and
"Ne pas répondre".
- AskUserQuestion is only disabled when interactive permissions are off;
the agent is told to use it when it needs a decision.
Verified with the real claude CLI (answered, skipped, and a regular
permission request) and the SQL on Postgres 16.
Co-Authored-By: RuFlo <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_01EYLzTmzezSkXAERkr41FBp
- Show 'Session Claude Code ouverte' only once (Claude Code re-emits init on every resume when async sub-agents report back). - Only announce task_notification for sub-agents; background commands (npm test, build) emit it too and showed up as 'Sous-agent sous-agent'. - Cost is cumulative: display it only on the final result line. - Headless note: sub-agents cannot message each other, so the agent is told to run them synchronously, relay each result itself, and not poll with ScheduleWakeup/ListAgents. Co-Authored-By: RuFlo <ruv@ruv.net> Claude-Session: https://claude.ai/code/session_01EYLzTmzezSkXAERkr41FBp
- "@architect @coder Ajoute…" limits the mission to those sub-agents: the main agent is told to delegate each step in order, synchronously, relaying each result to the next. - A PreToolUse hook (worker/agent-guard.ts, passed via --settings) denies any other sub-agent type and background launches. - The worker publishes the agents available in each folder (.claude/agents, user agents, built-ins) to maria.workspaces.agents (migration 0006); the mission form autocompletes @ mentions and previews the chain. Co-Authored-By: RuFlo <ruv@ruv.net> Claude-Session: https://claude.ai/code/session_01EYLzTmzezSkXAERkr41FBp
- Agents whose name contains spaces ("Benchmark Suite", "Performance
Monitor"…) are now listed and mentioned as @Benchmark-Suite; the chain
note and the guard keep the exact subagent_type.
- The suggestion menu shows every match (scrollable, keyboard-followed)
instead of the first 8, and also matches on the description.
Co-Authored-By: RuFlo <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_01EYLzTmzezSkXAERkr41FBp
- The worker reads memory_entries from each folder's .swarm/memory.db (read-only, node:sqlite) every 30 s and mirrors added, changed and removed entries into maria.memory_entries (migration 0007). - New "Mémoire" tab: per-folder entries with namespace filters, search on key/content/tags and expandable content. - MARIA_MEMORY_DB sets the database path (relative to each folder), or disables the sync with 0. Co-Authored-By: RuFlo <ruv@ruv.net> Claude-Session: https://claude.ai/code/session_01EYLzTmzezSkXAERkr41FBp
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Advanced Run ID:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Devin Review found 9 potential issues.
1 flag not posted on this PR by your GitHub settings — view it in Devin Review. (Configure)
| const parent = mission.parent_id ? await store.getMission(mission.parent_id) : null; | ||
| if (parent?.worktree_state === 'active' && parent.worktree_path && parent.branch && existsSync(parent.worktree_path)) { | ||
| const worktree: WorktreeInfo = { | ||
| path: parent.worktree_path, | ||
| cwd: await worktreeCwd(workspaceDir, parent.worktree_path), | ||
| branch: parent.branch, | ||
| baseCommit: parent.base_commit ?? '', | ||
| }; | ||
| await store.update(mission.id, { | ||
| branch: worktree.branch, | ||
| worktree_path: worktree.path, | ||
| base_commit: parent.base_commit, | ||
| worktree_state: 'active', | ||
| }); | ||
| sink.info(`Suite dans la branche ${worktree.branch}`); | ||
| return { cwd: worktree.cwd, worktree }; |
There was a problem hiding this comment.
🔴 Deux suites simultanées modifient la même branche isolée
Si deux suites sont lancées pour une même mission, elles réutilisent simultanément son worktree. Leurs modifications et commits se mélangent, contrairement à l'isolation promise.
Learn more
Une suite de mission peut reprendre le dossier et la branche de son parent. Le compteur de startMission limite seulement le nombre de missions par workspace, pas l'occupation de chaque worktree. Deux suites créées depuis un même parent passent donc toutes deux ici et exécutent Claude en parallèle sur les mêmes fichiers. Le contrôle worktreeBusy ne sert que lors d'une fusion ou d'un abandon, après l'exécution.
Example: Deux suites A et B sont lancées depuis la branche de la mission P. A et B lisent le même état, modifient le même fichier et exécutent chacune un commit ; la branche contient un mélange imprévisible de leurs travaux.
Recommended fix: Réserver atomiquement le chemin de worktree avant de lancer une suite, et ne démarrer qu'une seule mission qui écrit dans ce chemin ; coordonner aussi cette réservation avec la fusion et l'abandon.
Was this helpful? React with 👍 or 👎 to provide feedback.
| const sha = await commitWorktree(cwd, `MarIA: ${title}\n\nMission ${mission.id}`, [...cfg.worktreeLinks, ...cfg.worktreeCopies]); | ||
| sink.info(sha ? `Modifications commitées sur ${worktree.branch} (${sha.slice(0, 7)}).` : `Aucune modification à commiter sur ${worktree.branch}.`); | ||
| } catch (err) { | ||
| sink.info(`Commit sur ${worktree.branch} impossible : ${(err as Error).message}`, 'error'); | ||
| } |
There was a problem hiding this comment.
🔴 La fusion efface les modifications quand le commit a échoué
Si le commit automatique échoue, la mission reste fusionnable. La fusion puis la suppression forcée du worktree effacent ses modifications non commitées.
Learn more
Le worker tente de commiter les modifications d'une mission isolée, mais en cas d'échec il ne fait que publier un message. Il enregistre ensuite le statut terminal dans runMission, ce qui rend la fusion disponible dans BranchPanel. mergeWorktree fusionne les commits, puis removeWorktree supprime de force le dossier contenant les changements non commités.
Example: Le commit échoue parce qu'un fichier ajouté est illisible. L'utilisateur clique « Fusionner » : git fusionne la branche sans ce fichier et supprime le dossier où il se trouvait.
Recommended fix: Conserver l'erreur de commit dans l'état du worktree et bloquer toute fusion ou suppression forcée tant que le travail non commité n'a pas été sauvegardé ou que l'utilisateur n'a pas explicitement choisi de le perdre.
Was this helpful? React with 👍 or 👎 to provide feedback.
| if (worktree && !exit.error) { | ||
| const title = mission.prompt.split('\n')[0].slice(0, 72); | ||
| try { | ||
| const sha = await commitWorktree(cwd, `MarIA: ${title}\n\nMission ${mission.id}`, [...cfg.worktreeLinks, ...cfg.worktreeCopies]); |
There was a problem hiding this comment.
🔴 Les fichiers suivis exclus du commit disparaissent à la fusion
Une mission peut modifier .claude ou CLAUDE.md lorsqu'ils sont suivis par git. Leur exclusion systématique du commit laisse ces changements dans le worktree, que la fusion supprime ensuite.
Learn more
Par défaut, loadConfig inclut .claude et CLAUDE.md dans les copies. Quand ces chemins sont déjà suivis, createWorktree ne les copie pas, puisqu'ils existent dans le checkout git. Pourtant commitWorktree les exclut de git add -A, même si l'agent a modifié leurs versions suivies. Une fusion ultérieure supprime de force le worktree et ces modifications.
Example: Le dépôt suit CLAUDE.md. L'agent l'édite puis termine ; le commit n'inclut pas cette édition, et « Fusionner » la perd.
Recommended fix: N'exclure du commit que les liens et copies réellement ajoutés hors suivi lors de la création du worktree ; conserver l'index des chemins suivis par git dans la sélection à commiter.
Was this helpful? React with 👍 or 👎 to provide feedback.
| for (const mission of await store.pendingWorktreeActions(names.filter((n) => !mainBusy.has(n)))) { | ||
| if (mainBusy.has(mission.workspace)) continue; | ||
| mainBusy.add(mission.workspace); | ||
| track(runWorktreeAction(mission, cfg.workspaces[mission.workspace], store), () => mainBusy.delete(mission.workspace)); |
There was a problem hiding this comment.
🔴 Une erreur de fusion peut arrêter le worker
Si la lecture ou la mise à jour Supabase échoue pendant une action de branche, la promesse rejetée n'est pas interceptée. Le worker peut alors s'arrêter avec ses autres missions en cours.
Learn more
Le poller lance chaque action de branche dans runWorktreeAction via track, sans .catch. Le contrôle worktreeBusy est hors du try, et updateWorktree peut également échouer dans le bloc de rattrapage. track utilise .finally, qui conserve le rejet ; aucune autre fonction ne consomme cette promesse. Une panne Supabase transitoire crée donc un rejet non géré sous Node et peut tuer le worker.
Example: Supabase devient indisponible au moment de vérifier une demande « Fusionner ». worktreeBusy rejette ; le processus worker quitte alors que d'autres missions tournent.
Recommended fix: Intercepter explicitement toute erreur de runWorktreeAction avant track, la journaliser, et garantir que la promesse suivie se résout même quand l'enregistrement de l'erreur échoue.
Was this helpful? React with 👍 or 👎 to provide feedback.
| const entries = readMemory(path.resolve(dir, this.relPath), workspace) ?? []; | ||
| const changed = entries.filter((e) => known.get(e.id) !== Date.parse(e.updated_at ?? '')); | ||
| const current = new Set(entries.map((e) => e.id)); | ||
| const removed = [...known.keys()].filter((id) => !current.has(id)); |
There was a problem hiding this comment.
🟡 Une absence temporaire de la base efface la mémoire affichée
Si le fichier SQLite manque momentanément, la synchronisation traite cette absence comme une mémoire vide. Elle supprime alors toutes les entrées affichées dans Supabase.
Learn more
readMemory renvoie null lorsque le fichier n'existe pas, pour distinguer ce cas d'une table réellement vide. Le ?? [] efface cette distinction : les identifiants de toutes les entrées connues sont alors placés dans removed et supprimés de Supabase. Le fichier source n'est pas modifié, mais l'interface affiche une mémoire vidée jusqu'à une synchronisation réussie après son retour.
Example: .swarm/memory.db est renommé quelques secondes pour être remplacé ; un battement du worker arrive pendant l'intervalle et supprime les 100 entrées affichées.
Recommended fix: Ignorer la synchronisation lorsque readMemory renvoie null ; ne supprimer les entrées que lorsque la lecture d'une base présente confirme leur absence.
Was this helpful? React with 👍 or 👎 to provide feedback.
| Mission « {mission.prompt.length > 80 ? `${mission.prompt.slice(0, 80)}…` : mission.prompt} » · {mission.workspace} | ||
| </p> | ||
| )} | ||
| <QuestionForm questions={questions} busy={busy} onAnswer={answer} onSkip={() => decide(false)} /> |
There was a problem hiding this comment.
🟡 Une nouvelle question conserve les réponses de la précédente
Lorsque deux demandes arrivent successivement, le formulaire reste monté et conserve ses choix par index. La seconde question apparaît préremplie et peut envoyer une ancienne réponse.
| <QuestionForm questions={questions} busy={busy} onAnswer={answer} onSkip={() => decide(false)} /> | |
| <QuestionForm key={current.id} questions={questions} busy={busy} onAnswer={answer} onSkip={() => decide(false)} /> |
Was this helpful? React with 👍 or 👎 to provide feedback.
| export async function commitWorktree(cwd: string, message: string, excluded: string[]): Promise<string | null> { | ||
| const relToTop = await git(cwd, ['rev-parse', '--show-prefix']); | ||
| const excludes = excluded.map((name) => `:(top,exclude)${path.posix.join(relToTop, name)}`); | ||
| await git(cwd, ['add', '-A', '--', ':/', ...excludes]); |
There was a problem hiding this comment.
🟡 Le commit d'une mission englobe tout le dépôt
Quand le workspace est un sous-dossier, le commit ajoute aussi les modifications faites ailleurs dans le worktree. La fusion importe ainsi des changements étrangers à la mission.
Learn more
createWorktree prévoit explicitement qu'un workspace puisse être un sous-dossier du dépôt. Pourtant git add -A -- :/ cible la racine entière. Si l'agent ou un autre outil modifie un fichier frère dans ce worktree, celui-ci est inclus dans le commit automatique et dans la fusion, bien qu'il ne soit pas dans le dossier de travail configuré.
Example: Le workspace est repo/service-a, et un outil modifie repo/service-b/config.json depuis son checkout. Le commit automatique de la mission inclut aussi cette configuration.
Recommended fix: Limiter l'ajout aux chemins propres au workspace, ou rendre explicite et contrôler le choix d'un commit à l'échelle du dépôt ; vérifier les chemins déjà indexés avant de commiter.
Was this helpful? React with 👍 or 👎 to provide feedback.
| for (const name of opts.copies) { | ||
| const src = path.join(workspaceDir, name); | ||
| const dst = path.join(cwd, name); | ||
| if (existsSync(src) && !existsSync(dst)) cpSync(src, dst, { recursive: true }); | ||
| } | ||
| for (const name of opts.links) { | ||
| const src = path.join(workspaceDir, name); | ||
| const dst = path.join(cwd, name); | ||
| if (existsSync(src) && !existsSync(dst)) symlinkSync(src, dst); |
There was a problem hiding this comment.
🟥 Les chemins de copie des worktrees peuvent sortir du dossier autorisé
Un élément configuré avec .. peut pointer hors du workspace lors de la création d'un worktree. Le worker copie ou relie alors des fichiers extérieurs dans le dossier de l'agent.
Was this helpful? React with 👍 or 👎 to provide feedback.
| MARIA_ALLOWED_TOOLS=Read,Glob,Grep,Edit,Write,Bash(npm run *),Bash(npx ruflo *),Bash(git status),Bash(git diff *) | ||
| # Tout ce qui n'est pas listé est refusé (personne ne peut approuver en headless) ; les refus | ||
| # s'affichent en fin de mission dans MarIA. Élargis selon ce que tes missions doivent faire. | ||
| MARIA_ALLOWED_TOOLS=Read,Glob,Grep,Edit,Write,Bash(npm install *),Bash(npm run *),Bash(npm test *),Bash(npm --prefix *),Bash(npx *),Bash(node *),Bash(ls *),Bash(git status *),Bash(git diff *),Bash(git log *) |
There was a problem hiding this comment.
Follow-up to #1, which was merged right after the schema commit. This brings the rest of the MarIA work to
mainso Vercel can deploy it to production. Every change is underapps/maria/; Ruflo's core is untouched.What's included
worker/permission-mcp.ts) passed to--permission-prompt-tool. If that server fails to start, the feed reports it.AskUserQuestionopens a pop-up with options and free text. The answer goes back to the agent.MARIA_MAX_PARALLELrun at once per folder. Merge or discard from the UI.@architect @coder …restricts the mission to those sub-agents, delegated in order and synchronously. A PreToolUse hook blocks any other sub-agent and any background launch. The form autocompletes@from the folder's.claude/agents..swarm/memory.dbread-only (node:sqlite) and mirrors changes to Supabase every 30 s. A new "Mémoire" tab shows entries, with a namespace filter and search.cd,&&orgit -C;Database
Run migrations
0002to0007insupabase/migrations/in order, if they aren't applied yet. All 7 migrations were applied on Postgres 16 with a Supabase-like shim, and RLS was checked: members can read, outsiders see nothing, and the browser cannot write the worker's tables.Deployment (Vercel)
apps/maria.NEXT_PUBLIC_SUPABASE_URLandNEXT_PUBLIC_SUPABASE_ANON_KEY. Never setSUPABASE_SERVICE_ROLE_KEYon Vercel.Testing
npm run typecheckandnpm run buildpass.AskUserQuestionanswers come back to the agent;clauderunner: parallel worktree missions and an in-place regression run.memory.db: diff sync of additions, changes and removals.🤖 Generated with RuFlo
https://claude.ai/code/session_01EYLzTmzezSkXAERkr41FBp
Generated by Claude Code