Repository navigation
MarIA: permissions, agents, worktrees, questions, @-agent chains and Ruflo memory #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
797b4bb
fix(maria): require Node.js 22+ (native WebSocket needed by supabase-js)
claude 4807029
feat(maria): headless guidance for the agent and surface permission d…
claude 06d2095
chore(maria): log effective permission mode and allowed tools at work…
claude c78e69f
fix(maria): steer the headless agent away from git -C / --prefix forms
claude 20a6c36
feat(maria): interactive permission prompts (allow / deny window)
claude 602ba1a
fix(maria): diagnose a missing permission server instead of silently …
claude 65d1e8b
feat(maria): agents panel (Claude Code sub-agents + Ruflo registry)
claude 7199690
Merge branch 'ruvnet:main' into claude/zen-hawking-ioda4g
dr-phil-2004 6b1e79e
fix(maria): track async sub-agents until Claude Code reports them fin…
claude 57ade42
Merge remote-tracking branch 'origin/claude/zen-hawking-ioda4g' into …
claude a0417fb
feat(maria): isolated missions in git worktrees, run in parallel
claude fc798dc
fix(maria): disable AskUserQuestion in headless missions
claude cffab17
feat(maria): answer the agent's questions in a pop-up (AskUserQuestion)
claude 0784c4e
fix(maria): feed cleanup and sub-agent relay guidance
claude 063e41e
feat(maria): restrict missions to @-mentioned agents chained in order
claude 462ba3e
fix(maria): list every agent in the @ mention menu
claude bd1a840
feat(maria): Ruflo memory view
claude b527e56
Merge remote-tracking branch 'origin/main' into claude/zen-hawking-io…
claude File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| 22 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -24,6 +24,6 @@ | |
| "typescript": "^5.9.3" | ||
| }, | ||
| "engines": { | ||
| "node": ">=20.12" | ||
| "node": ">=22" | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,122 @@ | ||
| 'use client'; | ||
|
|
||
| import { useMemo, useRef, useState, type KeyboardEvent } from 'react'; | ||
| import { mentionAt, mentionName, parseMentions, type AgentInfo } from '@/lib/mentions'; | ||
|
|
||
| interface Props { | ||
| value: string; | ||
| onChange: (value: string) => void; | ||
| agents: AgentInfo[]; | ||
| placeholder: string; | ||
| rows: number; | ||
| onSubmit: () => void; | ||
| } | ||
|
|
||
| /** Zone de saisie de mission avec autocomplétion des mentions « @agent » et aperçu de la chaîne. */ | ||
| export function AgentPrompt({ value, onChange, agents, placeholder, rows, onSubmit }: Props) { | ||
| const ref = useRef<HTMLTextAreaElement>(null); | ||
| const [mention, setMention] = useState<{ start: number; query: string } | null>(null); | ||
| const [active, setActive] = useState(0); | ||
|
|
||
| const suggestions = useMemo(() => { | ||
| if (!mention) return []; | ||
| // Tous les agents correspondants (le menu défile) : ceux qui commencent par la saisie d'abord. | ||
| const q = mention.query.toLowerCase(); | ||
| const key = (a: AgentInfo) => mentionName(a.name).toLowerCase(); | ||
| const starts = agents.filter((a) => key(a).startsWith(q)); | ||
| const contains = agents.filter((a) => !key(a).startsWith(q) && (key(a).includes(q) || a.description.toLowerCase().includes(q))); | ||
| return [...starts, ...contains]; | ||
| }, [agents, mention]); | ||
|
|
||
| const chain = useMemo(() => parseMentions(value, agents.map((a) => a.name)), [value, agents]); | ||
|
|
||
| function refresh(text: string, caret: number | null) { | ||
| setMention(caret == null ? null : mentionAt(text, caret)); | ||
| setActive(0); | ||
| } | ||
|
|
||
| function pick(agent: AgentInfo) { | ||
| const el = ref.current; | ||
| if (!el || !mention) return; | ||
| const caret = el.selectionStart; | ||
| const insert = `@${mentionName(agent.name)} `; | ||
| const next = value.slice(0, mention.start) + insert + value.slice(caret); | ||
| onChange(next); | ||
| setMention(null); | ||
| const pos = mention.start + insert.length; | ||
| requestAnimationFrame(() => { | ||
| el.focus(); | ||
| el.setSelectionRange(pos, pos); | ||
| }); | ||
| } | ||
|
|
||
| function onKeyDown(e: KeyboardEvent<HTMLTextAreaElement>) { | ||
| if (suggestions.length > 0) { | ||
| if (e.key === 'ArrowDown' || e.key === 'ArrowUp') { | ||
| e.preventDefault(); | ||
| const step = e.key === 'ArrowDown' ? 1 : -1; | ||
| setActive((i) => (i + step + suggestions.length) % suggestions.length); | ||
| return; | ||
| } | ||
| if ((e.key === 'Enter' && !e.metaKey && !e.ctrlKey) || e.key === 'Tab') { | ||
| e.preventDefault(); | ||
| pick(suggestions[active]); | ||
| return; | ||
| } | ||
| if (e.key === 'Escape') { | ||
| e.preventDefault(); | ||
| setMention(null); | ||
| return; | ||
| } | ||
| } | ||
| if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) onSubmit(); | ||
| } | ||
|
|
||
| return ( | ||
| <div className="agent-prompt"> | ||
| <textarea | ||
| ref={ref} | ||
| placeholder={placeholder} | ||
| value={value} | ||
| rows={rows} | ||
| onChange={(e) => { | ||
| onChange(e.target.value); | ||
| refresh(e.target.value, e.target.selectionStart); | ||
| }} | ||
| onKeyDown={onKeyDown} | ||
| onClick={(e) => refresh(e.currentTarget.value, e.currentTarget.selectionStart)} | ||
| onBlur={() => setTimeout(() => setMention(null), 150)} | ||
| /> | ||
| {suggestions.length > 0 && ( | ||
| <ul className="mention-menu" role="listbox" aria-label={`${suggestions.length} agent(s)`}> | ||
| {suggestions.map((agent, i) => ( | ||
| <li | ||
| key={agent.name} | ||
| role="option" | ||
| aria-selected={i === active} | ||
| className={i === active ? 'active' : ''} | ||
| ref={i === active ? (el) => el?.scrollIntoView({ block: 'nearest' }) : undefined} | ||
| onMouseDown={(e) => { | ||
| e.preventDefault(); | ||
| pick(agent); | ||
| }} | ||
| onMouseEnter={() => setActive(i)} | ||
| > | ||
| <strong>@{mentionName(agent.name)}</strong> | ||
| {agent.description && <span className="muted"> — {agent.description}</span>} | ||
| </li> | ||
| ))} | ||
| </ul> | ||
| )} | ||
| {chain.agents.length > 0 && ( | ||
| <p className="chain small"> | ||
| Chaîne : {chain.agents.map((a) => `@${mentionName(a)}`).join(' → ')} | ||
| <span className="muted"> — seuls ces agents seront utilisés, dans cet ordre</span> | ||
| </p> | ||
| )} | ||
| {chain.unknown.length > 0 && agents.length > 0 && ( | ||
| <p className="muted small">Agent(s) inconnu(s) dans ce dossier, ignoré(s) : {chain.unknown.map((a) => `@${a}`).join(', ')}</p> | ||
| )} | ||
| </div> | ||
| ); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
| 'use client'; | ||
|
|
||
| import type { AgentsSummary, AgentStatus } from '@/lib/agents'; | ||
| import type { RufloAgent } from '@/lib/types'; | ||
|
|
||
| const STATUS: Record<AgentStatus, { label: string; className: string }> = { | ||
| running: { label: 'En cours', className: 'running' }, | ||
| done: { label: 'Terminé', className: 'completed' }, | ||
| error: { label: 'Erreur', className: 'failed' }, | ||
| interrupted: { label: 'Interrompu', className: 'cancelled' }, | ||
| }; | ||
|
|
||
| interface Props { | ||
| summary: AgentsSummary; | ||
| rufloAgents: RufloAgent[] | null; | ||
| finished: boolean; | ||
| } | ||
|
|
||
| /** Qui travaille sur la mission : agent principal, sous-agents Claude Code, et registre Ruflo. */ | ||
| export function AgentsPanel({ summary, rufloAgents, finished }: Props) { | ||
| const { mainActions, subAgents, rufloCalls } = summary; | ||
| const running = subAgents.filter((a) => a.status === 'running').length; | ||
| const rufloEntries = Object.entries(rufloCalls); | ||
|
|
||
| return ( | ||
| <div className="agents card"> | ||
| <h2> | ||
| Agents | ||
| {running > 0 && <span className="muted small"> · {running} en cours</span>} | ||
| </h2> | ||
| <ul className="agent-list"> | ||
| <li className="agent-item"> | ||
| <div className="agent-head"> | ||
| <span className="agent">MarIA</span> | ||
| <span className="muted small">agent principal · {mainActions} action(s)</span> | ||
| </div> | ||
| </li> | ||
| {subAgents.map((a) => ( | ||
| <li key={a.id} className={`agent-item ${a.parentId ? 'nested' : ''}`}> | ||
| <div className="agent-head"> | ||
| <span className="agent">{a.type}</span> | ||
| <span className={`badge ${STATUS[a.status].className}`}>{STATUS[a.status].label}</span> | ||
| </div> | ||
| {a.description && <div className="small">{a.description}</div>} | ||
| <div className="muted small agent-last"> | ||
| {a.actions} action(s){a.lastAction ? ` · ${a.lastAction}` : ''} | ||
| </div> | ||
| </li> | ||
| ))} | ||
| </ul> | ||
| {subAgents.length === 0 && ( | ||
| <p className="muted small">{finished ? 'Aucun sous-agent lancé.' : 'Pas encore de sous-agent.'}</p> | ||
| )} | ||
|
|
||
| {(rufloEntries.length > 0 || rufloAgents) && ( | ||
| <div className="ruflo"> | ||
| <h3 className="small">Ruflo</h3> | ||
| {rufloEntries.length > 0 && ( | ||
| <p className="muted small"> | ||
| Appels : {rufloEntries.map(([name, n]) => (n > 1 ? `${name} ×${n}` : name)).join(', ')} | ||
| </p> | ||
| )} | ||
| {rufloAgents === null && rufloEntries.length > 0 && ( | ||
| <p className="muted small">{finished ? 'Lecture du registre…' : 'Registre lu en fin de mission.'}</p> | ||
| )} | ||
| {rufloAgents && rufloAgents.length === 0 && <p className="muted small">Registre vide.</p>} | ||
| {rufloAgents && rufloAgents.length > 0 && ( | ||
| <ul className="agent-list"> | ||
| {rufloAgents.map((a) => ( | ||
| <li key={a.agentId} className="agent-item"> | ||
| <div className="agent-head"> | ||
| <span className="agent">{a.agentType}</span> | ||
| <span className="muted small">{a.status}</span> | ||
| </div> | ||
| <div className="muted small">{a.agentId}</div> | ||
| </li> | ||
| ))} | ||
| </ul> | ||
| )} | ||
| </div> | ||
| )} | ||
| </div> | ||
| ); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| 'use client'; | ||
|
|
||
| import { useState } from 'react'; | ||
| import { getSupabase } from '@/lib/supabase'; | ||
| import type { Mission } from '@/lib/types'; | ||
|
|
||
| const STATE_LABEL = { | ||
| active: { label: 'À valider', className: 'running' }, | ||
| merged: { label: 'Fusionnée', className: 'completed' }, | ||
| discarded: { label: 'Abandonnée', className: 'cancelled' }, | ||
| } as const; | ||
|
|
||
| /** Branche git isolée de la mission : fusion dans le dossier principal ou abandon. */ | ||
| export function BranchPanel({ mission, finished }: { mission: Mission; finished: boolean }) { | ||
| const [error, setError] = useState<string | null>(null); | ||
| if (!mission.branch || !mission.worktree_state) return null; | ||
|
|
||
| const state = STATE_LABEL[mission.worktree_state]; | ||
| const pending = mission.worktree_action; | ||
| const canAct = finished && mission.worktree_state === 'active' && !pending; | ||
|
|
||
| async function request(action: 'merge' | 'discard') { | ||
| const question = | ||
| action === 'merge' | ||
| ? `Fusionner ${mission.branch} dans la branche actuelle du dossier « ${mission.workspace} » ?` | ||
| : `Abandonner ${mission.branch} ? Le worktree et la branche seront supprimés, et les modifications perdues.`; | ||
| if (!window.confirm(question)) return; | ||
| setError(null); | ||
| const { error: err } = await getSupabase().rpc('request_worktree_action', { p_id: mission.id, p_action: action }); | ||
| if (err) setError(err.message); | ||
| } | ||
|
|
||
| return ( | ||
| <div className="branch card"> | ||
| <h2>Branche</h2> | ||
| <div className="agent-head"> | ||
| <code>{mission.branch}</code> | ||
| <span className={`badge ${state.className}`}>{state.label}</span> | ||
| </div> | ||
| {mission.worktree_path && mission.worktree_state === 'active' && ( | ||
| <p className="muted small">Worktree : {mission.worktree_path}</p> | ||
| )} | ||
| {!finished && mission.worktree_state === 'active' && ( | ||
| <p className="muted small">Les modifications seront commitées sur la branche à la fin de la mission.</p> | ||
| )} | ||
| {pending && <p className="muted small">{pending === 'merge' ? 'Fusion en cours…' : 'Suppression en cours…'}</p>} | ||
| {mission.worktree_error && <p className="error small">{mission.worktree_error}</p>} | ||
| {error && <p className="error small">{error}</p>} | ||
| {canAct && ( | ||
| <div className="row"> | ||
| <button onClick={() => request('merge')}>Fusionner</button> | ||
| <button className="danger" onClick={() => request('discard')}> | ||
| Abandonner | ||
| </button> | ||
| </div> | ||
| )} | ||
| </div> | ||
| ); | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟨 Les commandes préautorisées évitent la validation interactive
L'exemple préautorise
Bash(npx *)etBash(node *). Ces commandes peuvent exécuter du code arbitraire sans afficher la fenêtre de permission annoncée.Was this helpful? React with 👍 or 👎 to provide feedback.