Pași de pipeline
Un pas de pipeline are două axe independente:
- Tip (
StepKind) — cum programează motorul pasul în raport cu trigger-ele sale. - Cheie de corp (
BuiltInBodyKeys) — ce closure de corp înregistrat execută munca pasului.
Tipuri de pași
Secțiune intitulată „Tipuri de pași”Definite în packages/cc_domain/lib/features/pipelines/domain/entities/step_kind.dart.
| Tip | Semantica de programare |
|---|---|
trigger |
Punct de intrare obligatoriu — exact unul per șablon, întotdeauna primul nod. Nu face el însuși nicio muncă; ceea ce pornește rularea (manual / eveniment / program) este urmărit separat ca rânduri PipelineTrigger. Corpul său este pipeline.trigger, un no-op. |
listen |
Se declanșează când toți pașii sursă (din StepTrigger.sourceStepIds) se încheie. |
join |
Se declanșează când toți pașii din waitForStepIds ajung într-o stare terminală. |
router |
Ramificare condiționată — corpul returnează StepResult.route(key), care declanșează doar muchia din aval a cărei routeKey se potrivește; ramurile neselectate sunt marcate ca sărite. |
forEach |
Programat ca listen (se declanșează când toți sourceStepIds se încheie). Corpul înregistrat flow.forEach este cel care iterează; motorul nu creează câte un pas copil pentru fiecare element. |
terminal |
Santinelă a planificatorului, fără corp. Rularea pipeline-ului se încheie când un pas terminal se termină. |
Corpuri de pași
Secțiune intitulată „Corpuri de pași”Closure-uri de corp înregistrate, indexate după constantele din BuiltInBodyKeys (packages/cc_domain/lib/features/pipelines/domain/templates/builtin_template_seeds.dart). Sunt 25, toate enumerate mai jos. Configurația se citește din PipelineNodeConfig al pasului (vezi mai jos), dacă nu se specifică altfel; „cheile de stare” se citesc din starea pipeline-ului / payload-ul de trigger în momentul execuției.
Un pas al cărui bodyKey nu este înregistrat produce un avertisment la încărcarea șablonului și apoi nu se execută niciodată — nu face rularea să eșueze. Pașii trigger și terminal sunt exceptați de la acel avertisment: trigger-ul rulează totuși corpul no-op pipeline.trigger; un pas terminal nu are corp.
pipeline.trigger
Secțiune intitulată „pipeline.trigger”Corp no-op al nodului obligatoriu trigger — se încheie imediat, ca motorul să se ramifice către ascultătorii din aval. Fără configurație.
pipeline.bashScript
Secțiune intitulată „pipeline.bashScript”Pas shell fără agent (bash -c). Marcajele {{key}} din script sunt substituite din stare + payload-ul de trigger, cu valori escapate pentru shell.
| Câmp de configurare | Descriere |
|---|---|
script |
Comandă shell de executat (obligatoriu) |
outputKey |
Cheia de stare care primește stdout-ul curățat la ieșirea cu cod 0 |
Rulează cu cwd <cc_root>/pipelines/<pipelineRunId>/ (partajat de toți pașii bash dintr-o rulare) și cu GITHUB_TOKEN în mediu. Scrie de asemenea <stepId>_runDir în stare. O ieșire cu cod diferit de zero face pasul să eșueze, cu finalul din stderr.
conversation.promptAgent
Secțiune intitulată „conversation.promptAgent”Nod generic de tip prompt și trimitere: redă promptul, trimite agentul într-o conversație și suspendă până când rularea se încheie. Payload-ul submit_output al agentului este colectat în stare sub outputKey.
| Câmp de configurare | Descriere |
|---|---|
agentId |
UUID-ul agentului din spațiul de lucru care se trimite (obligatoriu) |
prompt |
Text de instrucțiune; acceptă substituția {{key}} (obligatoriu) |
outputKey |
Cheia de stare pentru ieșirea colectată |
outputSchema |
Schema JSON pe care trebuie să o respecte ieșirea |
label |
Eticheta afișată pe pânză |
extras.spaceId |
Conversația existentă în care se lucrează (implicit: una ascunsă) |
extras.conversationTitle |
Fluxul cu nume din interiorul acelui spațiu (opțional) |
extras.mode |
Se citește pe nodul care deschide camera (messaging.createSpace), nu aici. Implicit review. |
pipeline.condition
Secțiune intitulată „pipeline.condition”Corp de router — citește config.extras, evaluează o condiție și returnează StepResult.route(key). Condițiile nu sunt expresii JavaScript; sunt acceptate trei forme de redactare, în ordinea priorității:
- Arbore de predicate (
extras.predicate) — un arbore boolean care rutează"true"/"false". Fiecare nod este o hartă cu untype:{"type": "fileExists", "paths": [...], "baseKey": "repo_local_path", "negate": false, "recursive": false}— adevărat când oricare dintre căile listate există pe disc;negateîl inversează;recursivecaută și în subdirectoare (sărind peste.git,node_modules,build,.dart_tool). Căile relative se rezolvă față destate[baseKey](implicitrepo_local_path), altfel față de directorul de spațiu de lucru al rulării.{"type": "comparison", "left": "{{score}}", "op": "gt", "right": 80}— operatoriiequals,notEquals,contains,exists,notExists,gt,lt; citește starea pipeline-ului, nu sistemul de fișiere.{"type": "and" | "or", "of": [<predicate>, ...]}— grupuri booleene.{"type": "not", "of": <predicate>}— negație.
- Switch (
extras.switchKey) —{"switchKey": "pr_class", "cases": [...], "default": "standard"}; rutează către primul caz pe care valoarea de stare îl conține fără a ține cont de majuscule, altfel cătredefault. - Comparație (
extras.left/extras.op/extras.right, la nivel superior, variantă veche) — echivalentă cu un predicatcomparison, păstrată pentru șabloanele redactate înainte de arbore.
Muchiile care ies din nod poartă un routeKey care trebuie să corespundă cheii returnate.
prReview.comment
Secțiune intitulată „prReview.comment”Publică constatările consolidate ca un comentariu de revizuire pe PR, prin clientul GitHub de PR.
| Cheie de stare | Descriere |
|---|---|
repo_full_name |
owner/repo (obligatoriu) |
pr_number |
Numărul PR-ului — int sau șir numeric (obligatoriu) |
consolidated_findings |
Corpul comentariului (obligatoriu) |
Returnează StepResult.terminal, cu comment_review_id și commented_at în stare.
messaging.postSpace
Secțiune intitulată „messaging.postSpace”Publică un mesaj într-un spațiu de mesagerie prin MessagingPort.
| Cheie de stare | Descriere |
|---|---|
space_id |
Spațiul țintă (obligatoriu) |
content |
Corpul mesajului, citit textual din stare (obligatoriu) |
Scrie posted_space_id și posted_at în stare.
team.dispatch
Secțiune intitulată „team.dispatch”Trimite o echipă întreagă în loc de un singur agent, apoi suspendă până când rulările membrilor se încheie.
| Câmp de configurare | Descriere |
|---|---|
teamId |
Echipa din spațiul de lucru care se trimite (obligatoriu; exclusivă mutual cu agentId) |
prompt |
Textul obiectivului; acceptă substituția {{key}} (obligatoriu) |
dispatchMode |
allParallel (implicit — o sarcină per membru) sau manager (se trimite doar liderul, ca să coordoneze) |
outputKey |
Cheia de stare pentru ieșirile colectate ale membrilor |
outputSchema, label, extras.spaceId |
Ca la conversation.promptAgent |
human.gate
Secțiune intitulată „human.gate”Poartă de aprobare. Trimite agentul aprobator într-o conversație (modul review), cu promptul porții și un contract de ieșire {decision, reason}, apoi suspendă până când aprobatorul trimite prin submit_output.
| Câmp de configurare | Descriere |
|---|---|
agentId |
Agentul aprobator (obligatoriu) |
prompt |
Promptul porții (opțional; când lipsește, se folosește unul implicit) |
outputKey |
Cheia de stare pentru payload-ul {decision, reason} colectat |
outputSchema |
Implicit, o schemă de aprobare (decision: approved / rejected, reason) |
label, extras.spaceId |
Ca la conversation.promptAgent |
Poarta se rezolvă doar prin submit_output. BuiltInBodyKeys.humanGate, biblioteca de tipuri de noduri și textul de prompt al unui șablon inclus numesc toate uneltele MCP approve_step și reject_step; niciuna dintre aceste unelte nu există în vreun pachet sau registru.
repos.cleanup
Secțiune intitulată „repos.cleanup”Elimină worktree-urile izolate învechite; își alege modul din payload-ul de trigger. Respectă indicatorul dryRun al rulării.
| Cheie de stare | Descriere |
|---|---|
ticket_id |
Eliberează worktree-urile acelui tichet |
repo_full_name + pr_number |
Eliberează worktree-ul efemer al editorului de PR |
space_id |
Desființează worktree-urile și folderul acelei conversații |
| (niciuna dintre cele de mai sus) | Mătură spațiul de lucru: directoare dispărute, spații moarte, foldere orfane de conversații |
outputKey (opțional) primește un șir rezumat.
flow.forEach
Secțiune intitulată „flow.forEach”Mapare / fan-out peste o colecție de stare: redă promptul o dată pentru fiecare element, apoi trimite agentul o singură dată, cu acele prompturi concatenate (separator ---), și suspendă până când acea rulare se încheie. O colecție goală se încheie imediat, cu o listă goală. Nu este o trimitere paralelă per element.
| Câmp de configurare | Descriere |
|---|---|
agentId |
Agentul de trimis per element (obligatoriu) |
prompt |
Promptul per element; acceptă substituția {{key}} (obligatoriu) |
extras.iterableKey |
Cheia de stare care conține colecția (obligatoriu) |
extras.itemKey |
Cheia la care este legat elementul curent în prompt (implicit item) |
outputKey |
Cheia de stare pentru ieșirea colectată (o singură trimitere, nu o listă) |
flow.callPipeline
Secțiune intitulată „flow.callPipeline”Rulează un alt șablon de pipeline ca sub-pas imbricat (sub-pipeline-ul rulează ca rulare copil, cu parentPipelineRunId setat). Pasul suspendă până când copilul ajunge într-o stare terminală, apoi starea finală a copilului este fuzionată sub outputKey.
| Câmp de configurare | Descriere |
|---|---|
extras.templateId |
Șablonul copil de rulat (obligatoriu; un pipeline nu se poate apela pe sine) |
inputKeys |
Chei de stare copiate în payload-ul de trigger al copilului |
outputKey |
Cheia de stare pentru starea finală fuzionată a copilului |
code.index
Secțiune intitulată „code.index”Indexare tree-sitter în fundal: parcurge un depozit, extrage simboluri/muchii în izolate de worker și le ingeră în graful de cod al spațiului de lucru. Transmite progresul în flux către rândul de rulare al pasului; acceptă dry-run. Tree-sitter și gramaticile sale sunt native obligatorii — o bibliotecă dinamică lipsă înseamnă că serverul nu a pornit niciodată, iar o gramatică ce nu se poate rezolva în timpul rulării face pasul să eșueze, în loc să îl sară.
| Cheie de stare | Descriere |
|---|---|
repo_id |
Depozitul de indexat (obligatoriu) |
repo_local_path |
Calea locală a checkout-ului (obligatoriu) |
meeting.diarize
Secțiune intitulată „meeting.diarize”Diarizare offline a vorbitorilor. Citește audio păstrat al întâlnirii, îl grupează în vorbitori (Person 1, Person 2, …), reetichetează segmentele transcrierii și rescrie starea transcript; emite de asemenea diarization_spans pentru pasul paralel meeting.updateTranscript. Trece transcrierea mai departe nealterată când nu s-a păstrat niciun audio sau modelele nu sunt instalate.
| Cheie de stare | Descriere |
|---|---|
meeting_id |
Întâlnirea de diarizat (obligatoriu) |
meeting.saveNotes, meeting.addActionItems, meeting.addDecisions
Secțiune intitulată „meeting.saveNotes, meeting.addActionItems, meeting.addDecisions”Corpuri deterministe de persistare a rezumatului întâlnirii. Fiecare citește din stare meeting_id și payload-ul structurat meeting_outcome al pasului de agent ({summary, enhancedNotes, actionItems[], decisions[]}) și își scrie partea în propriul tabel — note, elemente de acțiune sau decizii. Rândurile salvate anterior nu sunt niciodată șterse într-o rulare degradată.
Alte corpuri incluse
Secțiune intitulată „Alte corpuri incluse”| Cheie de corp | Descriere |
|---|---|
meeting.identifySpeakers |
Recunoaștere a vorbitorilor între întâlniri: potrivește vorbitorii diarizați cu profilurile vocale salvate și aplică automat potrivirile sigure. No-op fără profiluri/embedding-uri. |
meeting.updateTranscript |
Aplică etichetele Person N din diarization_spans și fuzionează fragmentele per fereastră în replici per vorbitor, apoi persistă transcrierea curățată. No-op când diarizarea nu a produs nimic. |
meeting.assemblePlayback |
Asamblează pista de redare mixată a întâlnirii din fișierele WAV per canal păstrate. No-op când nu s-a păstrat niciun audio. |
orchestration.markPhase |
La finalul DAG-ului de lucru, scrie santinele de eșec pentru sub-tichetele fără ieșire și comută orchestrarea în synthesizing. |
orchestration.persistDeliverable |
Scrie ieșirea sintezei în tichetul părinte, îl finalizează, publică livrabilul și marchează orchestrarea ca completed. |
orchestration.awaitApproval |
Poartă de aprobare parțială: se încheie imediat când cheia de nod a pasului se află în setul aprobat al orchestrării, altfel suspendă până când o aprobare ulterioară o reia. Acesta este singurul corp care setează extras.awaitApproval, ceea ce îl scutește de timeout-ul de 24 de ore pentru pașii suspendați. |
messaging.createSpace |
Deschide conversația în care lucrează restul șablonului, publicată sub outputKey al pasului (corpurile incluse folosesc pipeline_space_id). O listă repoIds goală nu extrage nimic; extras['allRepos'] extrage fiecare depozit al spațiului de lucru. extras['createConversation'] (oprit implicit) deschide și un flux cu nume. Un pas de agent SE ALĂTURĂ unei camere și nu deschide niciodată una, așa că un fan-out fără acest nod eșuează la trimitere — iar cablarea lui o singură dată înseamnă că întregul fan-out partajează un singur checkout, în loc să provizioneze câte unul pentru fiecare. |
prReview.finalize |
Ultimul pas al șablonului de revizuire PR: sortează nodurile de revizuire acumulate în grupe de consens și calculează verdictul per PR. |
skills.analyze |
Rulează scanarea lanțului de aprovizionare pentru skill-uri peste skill-urile instalate ale unui spațiu de lucru și înregistrează fiecare verdict. |
Rezultate de pas
Secțiune intitulată „Rezultate de pas”Un corp de pas returnează un StepResult (packages/cc_domain/lib/features/pipelines/domain/entities/step_result.dart). Toate fabricile acceptă o hartă opțională mutatedState, fuzionată în starea pipeline-ului, cu excepția lui failed.
| Fabrică | Semnătură | Descriere |
|---|---|---|
ok |
StepResult.ok({mutatedState}) |
Încheiere normală; ascultătorii din aval sunt evaluați |
route |
StepResult.route(nextRouterKey, {mutatedState}) |
Încheiere de router; selectează ramura din aval a cărei routeKey se potrivește |
suspendUntilEvent |
StepResult.suspendUntilEvent(eventType, {mutatedState}) |
Suspendă până când se declanșează un anumit tip de eveniment de domeniu |
suspendUntilTasksComplete |
StepResult.suspendUntilTasksComplete(taskIds, {mutatedState}) |
Suspendă până când toate sarcinile listate ajung într-o stare terminală |
terminal |
StepResult.terminal({mutatedState}) |
Rularea pipeline-ului s-a încheiat — nu mai urmează pași |
failed |
StepResult.failed(errorMessage) |
Pasul a eșuat cu un mesaj de eroare |
StepRetryPolicy
Secțiune intitulată „StepRetryPolicy”Comportamentul de reîncercare pentru un corp de nod care eșuează (PipelineNodeConfig.retryPolicy; null dezactivează reîncercările).
| Câmp | Tip | Descriere |
|---|---|---|
maxAttempts |
int |
Numărul total de încercări, inclusiv prima (3 = 1 încercare + 2 reîncercări); trebuie să fie ≥ 1 |
backoff |
String |
linear sau exponential (implicit) |
initialDelayMs |
int |
Întârzierea înainte de prima reîncercare, în milisecunde; întârzierile ulterioare cresc conform backoff |
PipelineNodeConfig
Secțiune intitulată „PipelineNodeConfig”Configurația per nod, purtată în interiorul definiției pasului (packages/cc_domain/lib/features/pipelines/domain/entities/pipeline_node_config.dart). Atât nodurile incluse, cât și cele personalizate au aceeași formă; fiecare corp citește doar câmpurile care îl interesează.
| Câmp | Tip | Descriere |
|---|---|---|
prompt |
String? |
Șablon de prompt cu substituție {{key}} |
script |
String? |
Corp de script Bash cu substituție {{key}} |
agentId |
String? |
UUID-ul agentului din spațiul de lucru de trimis |
inputKeys |
List<String> |
Chei de stare pe care acest nod le consumă ca intrare |
repoIds |
List<String> |
Depozitele pe care le extrage nodul messaging.createSpace (<repoId> sau <repoId>@<branch>). Gol = niciunul; extras['allRepos'] = fiecare depozit al spațiului de lucru. Ignorat pe pașii de agent. |
outputKey |
String? |
Cheia de stare sub care se scrie ieșirea acestui nod |
label |
String? |
Eticheta afișată pe pânză (implicit, ID-ul pasului) |
outputSchema |
Map<String, dynamic>? |
Schema JSON (subset) pe care trebuie să o respecte valoarea de ieșire |
reducer |
String? |
Strategia de fuzionare când ramuri paralele scriu aceeași outputKey: append, mergeLists, mergeMaps, sum sau override (implicit) |
retryPolicy |
StepRetryPolicy? |
Politica de reîncercare la eșecul corpului; null = o singură încercare |
continueOnFail |
bool |
Când este adevărat, un eșec terminal nu face rularea să eșueze; eroarea este pusă deoparte sub state['_stepErrors'][stepId] |
timeoutMs |
int? |
Timeout în timp real pentru corpul nodului, în milisecunde; null = fără timeout |
teamId |
String? |
Echipa de trimis (nodurile team.dispatch); exclusivă mutual cu agentId |
dispatchMode |
String? |
Modul de execuție al echipei: allParallel sau manager |
extras |
Map<String, dynamic> |
Configurație liberă, specifică corpului (de ex. iterableKey, templateId, arbori de condiții) |
Accesori derivați: spaceId (extras['spaceId'] — conversația existentă în care se lucrează), createsConversation (extras['createConversation']), conversationTitle (extras['conversationTitle']), modeName (extras['mode'] — chat / review / plan, citit pe nodul care deschide spațiul) și runWhen (extras['runWhen'] — sare peste nod când condiția nu este îndeplinită).