Alle Anleitungen
Kanäle
Ein Kanal ist eine deklarierte Gruppe — und weil die Reihenfolge seiner Mitglieder verbindlich
gemacht werden kann, ist er zugleich der einzige Arbeitsablauf, den dieses System kennt. Beides steht
in einer Datei, .nxs-personas/channels.yaml, als YAML-Liste:
- name: review
members: [general, integrity]
description: the review quorum — one round asks both reviewers and hands back one verdictname und members sind die einzigen Pflichtschlüssel. Angesprochen wird er genau wie eine Persona:
nxc send --to review --ref nxf_ids=ab12.0008 "Judge the export change."Es gibt kein nxc channels create, und es wird nie eines geben. Ein Kanal, den ein Verb prägte,
lag in der Datenbank einer Maschine, hatte keine prüfbare Mitgliedschaft und überlebte den Lauf
nicht. Ein deklarierter Kanal ist eine Datei in Ihrem Repository — und deshalb ist members: die
Mitgliedschaft: es gibt nichts beizutreten und nichts zu verlassen.
Was ein Senden an einen Kanal wirklich tut
Ein Aufruf öffnet zwei Ebenen, und wer weiß, welche welche ist, versteht jede spätere Lesung von
nxc status sofort:
- Der Kanalfaden — Ihr Gespräch mit dem Kanal. Er hat genau zwei Enden: Sie und den Betreuer
des Kanals. Der Betreuer ist eine reservierte Engine-Identität,
<origin>/__channel__— er hat keine Sitzung, keinen Verlauf, und nichts nimmt ihn je wieder auf. Er ist Maschinerie, kein Beteiligter, und keine Deklaration kann seinen Namen beanspruchen, denn ein Handle darf nicht mit__beginnen. - Ein Platzfaden je Schritt — das Gespräch des Betreuers mit jedem Mitglied, unter den Kanalfaden gehängt. Das ist der Faden, in den ein Mitglied antwortet, und seine ID steht in der Anstoßnachricht des Mitglieds.
$ NXC_ACTOR=alice nxc send --to build-and-ship --ref nxf_ids=ab12.0008 "Ship the export change." --json
{"thread_id":"m-00000000000000000000000003","message_id":"m-00000000000000000000000004","to":"build-and-ship","target":"channel","channel":"decl:build-and-ship","expects":["ab12/__channel__"],"spawned":true,"warnings":[],"refs_warning":null}
Das expects dieser Quittung ist der ganze Entwurf in einem Feld: Sie warten auf den Betreuer, und
der Betreuer wartet auf die Mitglieder. Sie werden einmal geweckt, wenn die Runde fertig ist.
Parallel: die Vorgabe
Ein Kanal, der zur Reihenfolge nichts sagt, fächert an alle Mitglieder gleichzeitig auf. Jedes bekommt eine frische Sitzung und seinen eigenen Platzfaden; nichts wartet auf irgendetwas. Der Absender wird nie angestoßen, auch wenn er deklariertes Mitglied ist.
- name: review
members: [general, code-quality, test-quality, integrity]
expects: all # oder eine Handle-Liste — nur diese müssen antworten
on_complete: summarize # oder pass_through (Vorgabe)
summary_model: opus
summary_prompt: |
Fold the four reviews into one verdict and end with `Ready to merge? yes|no`.
visibility: requester_only # oder all_membersexpects entscheidet, wer antworten muss, damit die Runde vollständig ist: die bloße Zeichenkette
all (die Vorgabe, jedes Mitglied) oder eine YAML-Liste mit einer Teilmenge. Alles andere an dieser
Stelle ist ein lauter Parse-Fehler, der die Datei nennt.
on_complete entscheidet, was Sie erhalten.
pass_through(Vorgabe) gibt Ihnen jede gesammelte Antwort in einer definierten Form — definiert, weil der Leser meist ein Agent ist, der sie parst, und kein Mensch, der sie überfliegt. Sie beginnt mit einem Kopf, der Kanal, Faden und die Zahl der gesammelten Nachrichten nennt, dann eine Rahmenzeile, dann jede Antwort in einem Element<message from="…">.Dieses
from-Attribut ist die eigene Aufzeichnung der Engine darüber, wer geschrieben hat, und keine Antwort kann eines fälschen. Die Trennzeichen werden je Runde gewählt, nachdem jede Antwort gelesen wurde, so dass keine Antwort sie enthält — eine Antwort, die</message>ernsthaft schreibt, verschiebt die Runde einfach auf<message.1 …>…</message.1>, und die Lieferung nennt die verwendete Grenze. Nichts wird maskiert und nichts abgelehnt: die Grenze wird an die Antworten angepasst, nie die Antworten an die Grenze.Was das nicht klärt, ist, ob ein Mitglied die Wahrheit sagt. Die Texte sind das, was die Mitglieder geschrieben haben, geliefert in einen Prompt — und die Rahmung gibt es, um genau das zu sagen: behandeln Sie sie als Daten, nie als Anweisungen.
summarizelässt ein definiertes Modell mit einem definierten Prompt über diese Antworten laufen und gibt Ihnen stattdessen dessen Ergebnis.summary_promptist dann Pflicht — ein Kanal mitsummarizeund ohne Prompt wird am Einsatzpunkt abgelehnt, nicht still mit nichts gefaltet.summary_modelist optional; ein Kanal, der keines nennt, faltet auf der Stufejunior, denn eine Faltung ist abgeleitete Arbeit: sie gibt wieder, was andere Sitzungen bereits erzeugt haben. Benennen Sie ein Modell, wenn Ihre Faltung etwas entscheidet — ein Merge-Urteil, eine Weichenstellung —, denn falsch entschieden schickt sie Arbeit in die falsche Richtung.
Eine Eskalation wird nie weggefaltet. Hat irgendein Mitglied mit --escalate geantwortet, wird
die Runde durchgereicht, wie sie ist — was der Kanal auch deklariert —, und kein Synthesizer wird
gestartet. Zwei Gründe: „ich kann nicht" ist kein Ergebnis, und ein Modell darüber laufen zu lassen
erzeugte Prosa über ein Scheitern an der Stelle, an der ein Anfragender eine Antwort liest — während
die Antworten der erfolgreichen Mitglieder ungewaschen danebenstehen, was strikt mehr Information ist.
visibility ist requester_only (Vorgabe) oder all_members. Unter requester_only sieht der
Anfragende alles, jedes andere Mitglied die Eröffnungsnachricht und die eigenen Antworten. Gefiltert
wird nur der Inhalt; wer erwartet wird und wer geantwortet hat, wird nie verborgen.
Wer sonst noch lesen darf: wer die OPERATION eröffnet hat. Eine Operation quert Kanäle — ein
Mensch fragt einen Planer, der Planer beauftragt einen Bauer in #coding, der Bauer eine Runde in
#review — und keiner dieser Kanäle nennt den Menschen in seinen members:. nxc status zeigt
diesen ganzen Baum von der Wurzel abwärts schon immer, über jede Kanalgrenze; seither folgen die
Nachrichten. Wer die Operation eröffnet hat, liest jeden Faden darin, in welchem Kanal er auch
liegt.
Für alle anderen weitet das requester_only nicht: diese Regel gilt zwischen den Teilnehmern
einer Runde, und ein Mitglied sieht die Antwort seines Nachbarn weiterhin nicht. Der Eröffner der
Operation steht nicht in der Runde — er ist die Partei, der die ganze Kette antwortet, und jede
Zusammenführung wird ohnehin zu ihm hin geliefert.
Und members: ist wirklich die Mitgliedschaft, in beide Richtungen: eine Änderung an der Datei
erreicht das Tor beim nächsten Lesen, nicht beim nächsten send. Ein hinzugefügter Handle liest
sofort; ein entfernter liest nicht mehr, ohne dass dazwischen irgendetwas neu gesendet werden muss.
Am KANAL deklariert nichts, wann ein Mitglied fortgesetzt wird, und das ist auch nicht nötig:
ein Auffächern beginnt den Zug eines Mitglieds immer auf einer frischen Sitzung, und eine nxc reply --thread in den eigenen Faden dieses Mitglieds setzt die Sitzung fort, die es bereits hat. Bis zu
seiner Entfernung stand hier ein Schlüssel member_session: fresh | resume — er steuerte nichts und
lehnte seinen eigenen zweiten Wert ab —, und eine channels.yaml, die ihn noch setzt, lädt
unverändert; der Schlüssel wird ignoriert.
Die eine Stelle, an der eine Deklaration es doch entscheidet, ist ein Schritt: er hat ein
eigenes resume: — siehe Eine Sitzung fortsetzen oder frisch
beginnen. Je Schritt und nicht je Kanal, aus dem
Grund, an dem der entfernte Schlüssel scheiterte: was eine Runde mitnehmen soll, hängt davon ab,
über welche Kante sie ankam, und eine Antwort für den ganzen Kanal kann das nicht sagen.
Sequenziell: die Reihenfolge ist der Ablauf
- name: build-and-ship
members: [coder, review]
flow: sequential
description: the declared order a work order runs through
timeout: 2hEin Feld macht aus einer Gruppe einen Arbeitsablauf. flow: sequential sagt, dass die Reihenfolge von
members: bindet: ein Ziel nach dem anderen, jedes erst gestartet, wenn das davor erledigt ist.
flow: sequential hat bewusst keine eigene Schrittliste. Ein zweites Feld mit einer eigenen
Zielfolge wäre eine zweite Mitgliederliste neben members, mit eigenen referenziellen Regeln und
einer eigenen Art, der ersten zu widersprechen. members ist bereits eine geordnete Liste; was
fehlte, war keine Reihenfolge, sondern die Erklärung, dass die Reihenfolge bindet. Genau das sagt
dieses Feld, und alles Übrige über einen solchen Ablauf wird an members abgelesen.
Damit steht auch seine Grenze fest: eine flache Liste ist eine gerade Linie. Wer einen Zyklus
braucht — arbeiten, prüfen, zurück an die Arbeit — deklariert stattdessen steps:, der nächste
Abschnitt.
Das Ziel eines Schritts wird genau so angesprochen, wie send --to eines anspricht: ein deklariertes
Persona-Handle oder ein deklarierter Kanalname. So wird aus einer Review-Runde ein Schritt
statt etwas, das jemand von Hand öffnet — und was ein Kanalschritt beisteuert, ist seine eigene
konsolidierte Antwort.
Wie ein Schritt weiterrückt. Das Mitglied beantwortet seinen Platzfaden mit nxc reply --thread <id>. Diese Antwort ist der ganze Mechanismus: der Betreuer läuft im selben Schreibpfad mit, sieht,
dass die Menge erledigt ist, und öffnet den nächsten Schritt. Es gibt kein „Schritt fertig"-Verb,
kein Verzweigungswort und nichts abzufragen. Endet ein Mitglied seine Sitzung ohne zu antworten,
steht der Vorgang still, bis das deklarierte timeout zuschlägt.
„Erledigt" heißt complete || stale — ein Schritt, der geantwortet hat, und einer, dessen Fenster
mit nichts darin ablief, geben beide den nächsten frei. Ein Schritt, der stumm blieb, wird
gemeldet, nie geschluckt: der Aufruf, der den Ablauf an ihm vorbeibewegt hat, trägt eine Warnung
step_unanswered, die den stummen Faden benennt, und die Konsolidierung, die keine Antwort von ihm
trägt, tut es ebenso.
Ein Schritt, der eskaliert hat, gibt gar nichts frei — er beendet die Kette. nxc reply --escalate sagt „das kann ich nicht ausführen", und eine Aufgabe, die nicht ausgeführt wurde, darf
nicht die Arbeit beauftragen, die auf sie folgen sollte. Der Nachfolger wird also nicht gestartet,
die Runde geht so nach oben, wie sie ist, mit der Eskalation daran, und wer den Kanal beauftragt hat,
entscheidet, was nun geschieht. An einem parallel-Kanal ändert sich nichts: dort wurden alle auf
einmal gefragt, es gibt also keinen Nachfolger zurückzuhalten.
Eines hat an einem flow: sequential-Kanal keine Schreibweise, und man weiß es besser, bevor man es
braucht: ein Schritt kann dort nicht „noch nicht" sagen. reply hat zwei Antworten, und beide
beenden den Zug. Ein Schritt, der eine eigene Runde beauftragt hat — etwa eine Review — und den
Ablauf stehen lassen will, bis sie zurückkommt, wartet schlicht und antwortet, wenn das Ergebnis da
ist. Zu --escalate zu greifen, um den Ablauf zu halten, wirkt zwar — es kostet aber den Rest der
Kette und sagt dem Aufrufer, dass die Arbeit nicht kommt.
Ein stepped Kanal hat eine dritte Antwort, und dafür gibt es den nächsten Abschnitt.
…und bei working_tree: exclusive muss auch die SITZUNG des Schritts vorbei sein. Eine Antwort
ist eine Nachricht, und der Prozess, der sie geschrieben hat, kann weiterarbeiten: in einem echten
Lauf gemessen, antwortete ein Coder um 00:16:33 und schrieb bis 00:34:27 weiter, während der nächste
Schritt schon um 00:16:36 gestartet war — in genau die Arbeitskopie, die der Kanal für sich allein
deklariert hat. Ein Kanal, der die Arbeitskopie beansprucht, wartet deshalb auf beide Tatsachen. Die
zweite liefert die Sitzung selbst, indem sie beim Abbau ihr Ende meldet (nxc session ended, was
der Sidecar für Sie erledigt); bleibt das aus, antwortet die Prozessprüfung der Laufzeit beim
nächsten Mal, dass überhaupt jemand fragt — und es fragt jemand, auf einer Uhr, siehe unten. Ein shared-Kanal ist davon unberührt — zwei lebende
Sitzungen sind dort kein Fehler.
Eine Folge, die man besser vorher kennt: an einem solchen Kanal startet der nächste Schritt einen Moment nach der Antwort statt im selben Augenblick, denn er startet, wenn die vorige Sitzung endet.
Dieses Warten bewacht eine eigene Uhr. timeout: weiter unten kann es nicht: es ist eine Frist
für eine ANTWORT, und ein Mitglied, das geantwortet hat, hat geantwortet. Eine Ablehnung, deren
einziger verbliebener Hinderungsgrund eine lebende Sitzung ist, plant deshalb eine Minute später
eine Nachprüfung desselben Bretts ein — und tut das weiter, solange der Prozess noch da ist. In der
Praxis kommt die Meldung eine Sekunde später und Sie sehen von alldem nichts. Wenn nicht — ein vor
dem Abbau getöteter Sidecar, ein nxc älter als diese Funktion, eine Wirt-Laufzeit, die
Engine::session_ended nie aufruft —, findet die Nachprüfung den Prozess weg, und der Ablauf geht
von selbst weiter, rund eine Minute später.
Von Hand fragen können Sie immer, und die Antwort ist eine Diagnose statt eines Achselzuckens:
$ nxc tick --thread <der Kanalfaden>
tick declined on thread …: the set is settled, but a session behind it is still running.Welche Sitzung das ist und was aus ihr geworden ist, sagt
nxc session state --thread <der Faden des Schritts>.
Ein Prozess, der hängt statt zu enden, ist der eine Fall, den die Uhr nicht auflösen kann: sie
schaut weiter nach und findet ihn weiter lebend — dasselbe Warten wie vorher, aber ein sichtbares,
auf das etwas schaut. Wer eine Wirt-Laufzeit baut, hält sich mit Engine::session_ended aus
alldem heraus.
Zwei Dinge werden an einem sequenziellen Kanal abgelehnt, beide am Einsatzpunkt und beide, ohne dass etwas gestartet wäre:
expects: <teilmenge>— die Auffächerung gibt eine Teilmenge unverändert zurück, sie wäre also eine zweite Liste, die die Reihenfolge still bestimmt. Die Ablehnung nennt beide Listen.- dasselbe Ziel zweimal in
members, sowie jede Menge von Kanälen, die sich über ihre Mitglieder selbst erreichen kann. Ein zyklischer Katalog wird beim Laden der Deklarationen rundheraus abgelehnt.
Die zweite Ablehnung ist keine Ordnungsliebe: an einem solchen Kanal ordnet der Motor einen Faden
seinem Schritt zu, indem er dessen Ziel gegen members abgleicht — eine Wiederholung machte zwei
verschiedene Schritte ununterscheidbar. steps: unten hebt sie auf, indem es jedem Schritt einen
Namen gibt.
Schritte: wenn der Ablauf zurückkommen muss
- name: coding
members: [coder, review, finisher]
working_tree: exclusive
steps:
- id: build
target: coder
next: check
- id: check
target: review # eine Persona oder ein ganzer Kanal
on_needs_rework: build # der Weg ZURÜCK
max_passes: 3 # Durchgänge INSGESAMT — der ursprüngliche plus zwei Nacharbeiten
next: ship
- id: ship
target: finishersteps: ist eine kleine Zustandsmaschine. members: behält seine bisherige Bedeutung — wer zu
diesem Kanal gehört — und steps: sagt, was in welcher Reihenfolge geschieht. Ein Kanal mit
steps: darf nicht zusätzlich flow: sequential deklarieren: das wären zwei Listen, die dasselbe
entscheiden.
idbenennt den Schritt, und erst das macht einen Zyklus möglich. Zwei Schritte dürfen dieselbe Persona ansprechen, und derselbe Schritt darf in einer Runde zweimal laufen, weil der Motor den Faden eines Schritts an dessenidfestmacht statt am Namen seines Ziels.nextist der Normalweg. Ein Schritt ohnenextist das Ende der Runde: dort konsolidiert der Kanal und antwortet dem, der gefragt hat.on_needs_reworkist der Weg zurück, und die einzige Abzweigung, die es gibt.max_passeszählt Durchgänge insgesamt, nicht Nacharbeiten: der ursprüngliche Lauf ist Durchgang 1,max_passes: 3erlaubt also zwei Wege zurück. Alles unter2wird abgelehnt — es erklärte eine Rückkante, die nie genommen werden kann, und ein Schritt, der nie zurückschicken soll, deklariert schlicht keinon_needs_rework. Es sitzt an der Rückkante, nie am Kanal: ein Kanal kann mehrere Zyklen haben, und ein Kanalzähler würde sie zusammenzählen. Es ist Pflicht, wo die Kante steht — eine Rückkante ohne Decke ist eine Schleife, die nichts beendet.resumeentscheidet, ob der Schritt die Sitzung FORTSETZT, die sein Ziel in dieser Runde schon geführt hat, oder eine frische beginnt. Lässt man es weg, entscheidet die Kante — und das ist fast immer das, was man will; siehe unten.
Eine Sitzung fortsetzen oder frisch beginnen
Ein Schritt, der über on_needs_rework: erreicht wird, ist dieselbe Stelle, die ihre eigene Arbeit
zurückbekommt. Sie wird mit einem Hinweis geweckt, der sagt „was Du übergeben hast" und „Dein
Anspruch auf die Arbeitskopie ist weiterhin Deiner" — Sätze, die nur für die Sitzung wahr sind, die
tatsächlich etwas übergeben hat. Deshalb setzt eine Rückkante diese Sitzung fort: die Befunde
kommen als ihr nächster Zug an, alles Gebaute steht noch in ihrem Kontext, und sie muss ihre eigene
Arbeit nicht erst wieder aus der Arbeitskopie herleiten.
Ein Schritt, der über next: erreicht wird, ist das nächste Stück Arbeit. Dass er zufällig dieselbe
Persona nennt, ist für sich kein Grund, eine Sitzung mitzunehmen — deshalb beginnt eine Vorwärtskante
frisch.
Das ist die Vorgabe, und resume: am Schritt sticht sie in beide Richtungen — dieselben zwei
Schritte wie oben, beide Vorgaben umgedreht:
- id: build
target: coder
next: check
resume: false # eine Nacharbeit geht jedes Mal in eine SAUBERE Sitzung
- id: check
target: review
on_needs_rework: build
max_passes: 3
next: ship
resume: true # … während der Prüfer seine Sitzung über die Durchgänge behältEin Prüfer, der seine Sitzung behält, ist keine Spielerei: beim zweiten Blick weiß er noch, was er verlangt hat, und kann beurteilen, ob es geschehen ist.
In beiden Fällen bekommt der Schritt einen frischen Faden — das ist es, was der
Durchgangszähler zählt. Fortsetzen betrifft die Sitzung, nicht den Platz. Eine Runde, die dieses Ziel
noch nie geführt hat, hat nichts fortzusetzen und beginnt schlicht — das ist der erste Durchgang und
kein Fehler. Und ein Schritt, dessen Ziel ein ganzer Kanal ist, lässt sich gar nicht fortsetzen:
ein Kanal hat keine einzelne Sitzung hinter sich, seine Mitglieder haben je eine eigene. resume: true wird dort beim Laden des Teams als fehlerhafte Deklaration gemeldet.
Die dritte Antwort
Ein Schritt, dessen Deklaration eine on_needs_rework:-Kante nennt, gibt der Stelle, die ihn
bedient, einen dritten Weg, ihren Zug zu beenden:
nxc reply --thread <id> "<dein Ergebnis>" # fertig
nxc reply --thread <id> --needs-rework "<was in Ordnung zu bringen ist>" # fremde Arbeit geht zurück
nxc reply --thread <id> --escalate "<was im Weg steht>" # ich kann nicht; die Runde geht hochDie drei sind eine Trichotomie über erledigt, nochmal, kann nicht, und einen vierten Fall gibt
es nicht. „Gut, aber unvollständig" ist Nachbesserung. „Ich brauche erst X" ist Eskalation. „Passt"
ist der Normalweg. Man beachte, um wessen Arbeit es jeweils geht: --escalate ist eine Aussage über
die eigene Arbeit, --needs-rework über fremde. Beides zugleich wird abgelehnt.
Einem Schritt ohne Rückkante wird das Bit gar nicht erst angeboten — seine Sitzung erfährt von
zwei Ausgängen, nicht von dreien. Das ist Absicht: zeigt der Ablauf auf eine Wertung keine Reaktion,
dann war um eine Einschätzung gebeten, nicht um eine Entscheidung. Wer es dort dennoch setzt (über
--help gefunden), dessen Wertung fällt weg — mit einer Warnung verdict_dropped auf der Quittung
genau dieser Antwort, die sagt, dass dieser Kanal dafür keinen Übergang erklärt.
Ist das Ziel des Schritts ein ganzer Kanal, genügt ein einziges Mitglied. Ein Prüfer, der etwas
gefunden hat, wird nicht von dreien überstimmt, die nichts gefunden haben — und das eigene
on_complete: summarize des Kanals faltet die Antworten weiterhin zu dem einen Urteil, für das er
deklariert wurde.
Was mit der Arbeit zurückkommt
Die zurückgeschickte Stelle wird mit der Wertung selbst geweckt — die Befunde sind die Aufgabe — unter einem Hinweis, der sagt, was geschehen ist, der wievielte Anlauf das ist und dass ihr Anspruch auf die Arbeitskopie weiterhin ihrer ist:
NEEDS REWORK — this is NOT an approval. What you handed over was reviewed and did not meet the
standard of the party that reviewed it; their reasons are in the body below, and they are the work.
This is pass 2 of 3. Your claim on the working copy is STILL YOURS and is held across this round — do
not acquire it again. …Mit rework_notice: am Kanal sagt man es in eigenen Worten; {pass} und {max} werden eingesetzt.
Wenn die Decke erreicht ist
Die Runde wird nach oben eskaliert, nicht still gestoppt: kein weiterer Anlauf wird gestartet, jede Antwort der Runde wandert zu dem, der den Kanal beauftragt hat, und die Zustellung nennt die erreichte Decke. Diese Stelle entscheidet. Die Runde erneut zu beauftragen ist eine neue Runde — eine Antwort in den Kanalfaden — und eine neue Runde beginnt ihre Zählung bei eins; mehr „Zurücksetzen" als eine Freigabe von oben gibt es nicht.
Was die Runde am Ende berichtet
Eine Runde, die über eine Rückkante gegangen ist, übergibt zwei Antworten desselben Schritts, die einander absichtlich widersprechen — „the lock order is wrong" und „good now". Deshalb kommen die Antworten nicht als flache Liste an: jede trägt den Schritt, dem sie diente, und den wievielten Durchgang dieses Schritts sie war, und die Antwort, die die Wertung trug, ist als solche markiert.
<message from="local/coder" step="build" pass="1">
first cut is in: added the cache
</message>
<message from="local/review" step="check" pass="1" needs_rework="true">
the lock order is wrong in two places
</message>
<message from="local/coder" step="build" pass="2">
lock order fixed
</message>Über dem Block sagt die Zustellung die eine Sache, die sich aus den Nachrichten selbst nicht herleiten lässt: taucht ein Schritt mehrfach auf, löst der spätere Durchgang den früheren ab — die früheren Durchgänge sind der Weg hierher, keine Befunde, die noch stehen.
An derselben Stelle nennt sie außerdem, wie oft jeder Schritt wirklich gelaufen ist — vom Motor aus
seinem eigenen Protokoll gezählt. Das ist die eine Sache, die ein Leser nicht durch Lesen ermitteln
kann: mit wie vielen Durchgängen zu rechnen ist, und genau das macht „der spätere Durchgang löst den
früheren ab" zu einer Regel über eine bekannte Zahl von Dingen. Die Marken selbst sind die des
Motors, und keine Antwort kann eine schreiben — die Grenze, die das garantiert, steht oben bei
pass_through.
Beide Ausgabeformen tragen es. on_complete: pass_through zeigt es dem, der gefragt hat;
on_complete: summarize übergibt dieselbe Struktur der Sitzung, die den Abschlussbericht schreibt —
was zurückkommt, ist also aus der Runde gebaut statt durchgereicht. Das summary_prompt: sagt,
wozu der Bericht dient; die Gestalt des Laufs wird mitgeliefert.
Ein Kanal ohne steps: hat keine Durchgänge, und seine Zustellung ist genau die, die sie immer war.
Fristen
timeout: ist, wie lange eine Runde warten darf. Es wird hier deklariert oder nirgends — die
Übersteuerung --deadline je Aufruf ist entfallen, weil ein deklariertes Fenster und die
Übersteuerung eines Aufrufers zwei Antworten auf eine Frage sind.
timeout: 20m # ein Fenster: 20 Minuten Stille
timeout: 2026-08-16T10:30:00Z # ein Zeitpunkt: dieser Moment, was auch geschiehtDer Parser versucht RFC3339 zuerst, beide Formen sind also in einem Feld ausdrückbar — und der Unterschied ist real. Eine Dauer ist ein Fenster je Mitglied und rücksetzbar: die Uhr jedes Mitglieds wird von jedem Schreiben in dessen eigenen Sitzungsverlauf neu gestartet, ein Mitglied, das arbeitet, wird also nie dafür bestraft, dass es dauert. Wovor es schützt, ist ein Mitglied, das verstummt ist. Ein absoluter Zeitpunkt bewaffnet keine Uhr, und nichts verschiebt ihn.
Ein Wert, den die Grammatik nicht lesen kann, ist ein validation-Fehler, der Feld und Wert nennt,
abgelehnt bevor irgendetwas persistiert wird — nie ein stilles „dann gibt es eben keine Kappe".
Einheiten sind s, m, h, d, w.
Ist ein Fenster deklariert, stellt chat eine Frist auf den Moment, in dem es erstmals fällig wird.
Ist der Moment da, läuft ein verstecktes Verb: nxc tick --thread <id>. Es prüft den Faden erneut
und leitet ihn, wenn fällig, durch die Politik des Kanals. Weil sich das Fenster mit jedem
Lebenszeichen der Mitglieder verschiebt, bewaffnet sich die Kette selbst neu: ein Tick, der nichts
Fälliges findet, stellt die nächste Frist. Er ist idempotent — ein bereits behandelter Faden ist ein
sauberer No-Op, nie ein zweites Wecken.
Niemand tippt tick. Es steht hier, damit es kein Rätsel ist, wenn man es in einem Protokoll oder
einer Prozessliste findet.
Die Frist hält der nexus-flow-Hintergrunddienst
Eine Frist zu stellen schreibt eine Zeile in das Buch des Arbeitsbereichs, .nxs/timers.json. Der
nexus-flow-Hintergrunddienst — ein Prozess, derselbe, der Ihre Arbeitsbereiche synchronisiert —
liest dieses Buch bei jedem Durchgang und führt die Prüfung aus, wenn es so weit ist.
Standardmäßig heisst er nexus-flow, und auf einem Mac ist das der Name, den Sie in den
Systemeinstellungen › Allgemein › Anmeldeobjekte finden. Einmal einrichten:
nxs sync daemon install # macOS: ein launchd-Agent, der ihn bei der Anmeldung startet
nxs sync daemon # überall: im Vordergrund, unter der eigenen ProzessverwaltungSein Protokoll liegt in ~/.nexusflow/logs/service.log. nexus-flow ist auch ein Befehl — er ist
nxs sync daemon —, nexus-flow status beantwortet also „läuft er, und was hat er zuletzt getan".
Er hält den Rechner wach, solange ein Lauf arbeitet
Ein Lauf, der stundenlang unbeaufsichtigt arbeitet, überlebt es nicht, wenn der Mac einschläft.
Solange in irgendeinem betreuten Arbeitsbereich eine Sitzung lebt, hält der Dienst eine
Ruhezustands-Sperre — und gibt sie in dem Moment zurück, in dem die letzte vorbei ist. Sie können
sie unter ihrem Namen in pmset -g assertions sehen; dort steht auch, welche Instanz sie hält.
Sie verhindert nur den Ruhezustand aus Untätigkeit: Ihr Bildschirm schläft weiterhin ein, ein zugeklapptes Gerät schläft weiterhin ein, und „Ruhezustand" aus dem Menü funktioniert weiterhin. Und sie wird vom Dienstprozess selbst gehalten — stirbt der Dienst, stirbt die Sperre mit ihm, ein Absturz kann Ihren Mac also nicht dauerhaft wachhalten.
Mehrere Dienste auf einem Rechner
Wer an mehreren Arbeitskopien gleichzeitig entwickelt, will nicht, dass sie sich eine Uhr teilen:
wer zuletzt installiert, besäße sie — für alle anderen mit. Geben Sie einer Arbeitskopie einen
eigenen Instanznamen, und sie bekommt alles Eigene: einen eigenen launchd-Agenten, ein eigenes
Verzeichnis ~/.nexusflow-<name> und damit eine eigene Registry, einen eigenen Riegel, einen eigenen
Herzschlag und ein eigenes Protokoll:
export NXS_SERVICE_INSTANCE=nexus-flow-dev # in Ihrer .envrc, einmal je Arbeitskopie
nxs sync daemon install # installiert *diese* Instanz
nxs sync daemon status # sagt, für welche Instanz er antwortetEin Name ist nexus-flow oder nexus-flow-<etwas>. Der Name der Instanz ist zugleich der Befehl:
nexus-flow-dev status ist das status genau dieses Dienstes — und er ist auch, woran der laufende
Dienst erkennt, welcher er ist: der launchd-Agent fährt eine Verknüpfung dieses Namens, und der Name
ist die ganze Anweisung.
Zwei Instanzen können einander nicht in die Quere kommen: getrennte Verzeichnisse heißen getrennte
Riegel, also laufen beide nebeneinander, und die Installation der einen entfernt die andere nie. Das
Einzige, was sie sich teilen können, ist ein Arbeitsbereich — nichts hindert Sie daran, dieselbe
Arbeitskopie bei beiden anzumelden — und dann betreuen ihn beide, lesen beide sein Fristenbuch, und
eine fällige Frist kann zweimal gestartet werden. Melden Sie einen Arbeitsbereich bei genau einer
Instanz an; nxs sync bind, nxs sync daemon status und der laufende Dienst sagen es, wenn Sie es
nicht getan haben.
Diese Antwort sagt außerdem, welche Binärdatei er fährt: das Programm, aus dem der laufende
Dienst gestartet ist, dessen Version, und worauf die nexus-flow-Verknüpfung jetzt zeigt. Beides
kann auseinanderlaufen — nxs self-update schreibt eine neue Binärdatei und verschiebt die
Verknüpfung bewusst nicht, welcher Bau die Uhr dieser Maschine stellt, bleibt also eine Entscheidung
—, und status sagt es, wenn sie auseinandergelaufen sind. Zeigt die Verknüpfung auf eine Datei,
die es nicht mehr gibt, ist das der eine Fehler, den launchd überhaupt nicht melden kann (sein
einziges Symptom ist ein Dienst, der nie startet). Dieser Fall bleibt deshalb nicht status
überlassen: jeder Aufruf von nxs, nxf, nxm und nxc sagt ihn, benennt das tote Ziel und nennt
den einen Befehl, der ihn behebt.
Was das bringt, verglichen damit, für jede Frist einen Job beim Betriebssystem zu bestellen (so war es vorher):
- Sie gilt auf die Sekunde. Nichts wird auf die nächste volle Minute aufgerundet.
- Ein früh geschlossenes Brett lässt nichts zurück. Die Zeile wird überschrieben oder entfernt; kein Agent bleibt in Ihrer Anmeldesitzung stehen für ein Fenster, das es nicht mehr gibt.
- Überall dieselbe Mechanik. macOS, Linux und alles andere, worauf der Dienst läuft, lesen dasselbe Buch.
Das hat sich geändert, und es lohnt sich zu wissen, falls Sie es umgangen haben. nxc plante
früher über at — und macOS liefert atrun deaktiviert aus, also nahm at den Job an, beendete
sich mit 0, druckte eine Job-Nummer, und niemand führte ihn je aus. Auf einem Mac ab Werk stapelten
sich Jobs, deren Termin längst verstrichen war, und ein deklariertes timeout: feuerte dort nie:
ein Mitglied, das verstummte, hielt seine Runde unbegrenzt an, weil nichts das Brett je stale
werden liess. Wer dafür atrun von Hand aktiviert hat, braucht das nicht mehr. NXC_TIMER=at wählt
den at-Zeitgeber weiterhin ausdrücklich, auf jeder Plattform, falls Sie ihn wollen.
Und was es kostet, ausgesprochen
Ohne Dienst keine Uhr. at trug sich selbst — den Job abgeben, und das Betriebssystem besitzt ihn,
ob von uns etwas läuft oder nicht. Das hier tut das nicht, und nxc tut nicht so als ob: eine Frist
in einem Arbeitsbereich zu stellen, den kein Dienst betreut, oder während keiner läuft, trägt die
Frist trotzdem ein (ein später gestarteter Dienst hält sie) und meldet es zugleich auf der
Quittung, als Eintrag tick_unscheduled im warnings-Array, mit dem betroffenen Brett und dem, was
zu tun ist. Ein von Null verschiedener Exit-Code ist es bewusst nicht: das Brett ist offen und seine
Mitglieder laufen — was fehlt, ist die Sicherung.
Zwei Fälle unterscheidet es, weil sie verschiedene Antworten brauchen:
- dieser Arbeitsbereich ist beim Dienst nicht angemeldet — anmelden (
nxs sync bind, oder aus Ihrer App), dannnxs sync daemon install; - es läuft kein Dienst — starten.
Wenn Sie ihn doch sehen: nxc tick --thread <id> ist dasselbe Verb, das der Dienst ausgeführt hätte,
idempotent und jederzeit aufrufbar. Nichts geht verloren — das Fenster wird geprüft, wenn jemand
fragt, statt von selbst.
Wenn eine Runde nicht hängen bleiben darf und Sie den Dienst nicht laufen lassen können, verlassen
Sie sich dafür nicht auf timeout: — behandeln Sie die Sicherung als abwesend und sehen Sie auf dem
Brett nach.
Alleinnutzung der Arbeitskopie
- name: review
members: [general, code-quality, test-quality, integrity]
flow: sequential
working_tree: exclusiveAm Kanal deklariert, wenn die Runde das Repository braucht und nicht ein einzelnes Mitglied für
sich — ein Review-Quorum, in dem jedes Mitglied die Gates des Projekts selbst laufen lässt, ist genau
diese Form. Es geht auch andersherum: sagt ein deklariertes Mitglied für sich working_tree: exclusive, gilt der ganze Kanal als bedürftig, und diese Zeile wird nicht wiederholt. Diese Vererbung
gilt nur einen Sprung — ein Kanal fängt sie sich nicht über ein gemeinsames Mitglied bei einem
anderen Kanal ein.
Wovor sie schützt und wovor nicht: Sie schützt diese Runde vor anderen Ketten, die dann warten, statt ihren eigenen Build gegen dasselbe Zielverzeichnis laufen zu lassen. Sie trennt die Mitglieder einer Runde nicht voneinander — sie sind ein Anspruchsgebiet, das erste erwirbt und die übrigen erben, also laufen alle gleichzeitig. Die Einheit der Ausschließung ist das Gebiet, nicht die Sitzung.
Deshalb steht im Beispiel oben auch flow: sequential, und das ist keine Zierde. Vier Mitglieder,
die jedes den Build des Projekts in derselben Arbeitskopie laufen lassen, sind vier Builds in einem
Zielverzeichnis. Zwei gleicher Form nehmen bloß die Sperre des Build-Werkzeugs und warten aufeinander
— richtig, und um ein Vielfaches langsamer. Zwei verschiedener Form überschreiben einander die
Artefakte, und was Sie dann lesen, ist ein kaputter Build, der in Wahrheit ein zerschossenes Artefakt
ist. working_tree: kann sie nicht trennen; die Reihenfolge kann es, denn der nächste Schritt eines
geordneten Ablaufs öffnet erst, wenn der vorige geantwortet hat und seine Sitzung vorbei ist.
Also: ein Quorum, dessen Mitglieder nur lesen, darf auffächern und gibt Ihnen vier Meinungen auf
einmal; ein Quorum, dessen Mitglieder BAUEN, will beide Zeilen. Der Preis der zweiten ist Wanduhrzeit
— und die Regel geordneter Abläufe, dass eine eskalierende Antwort die Kette beendet: ein Mitglied,
das nicht laufen kann, hält die Runde an, statt die Urteile der anderen übrig zu lassen. Das ist
meist genau das, was man will, wenn ohnehin alle dieselbe Arbeitskopie teilen.
Und das Gebiet ist die OPERATION — der ganze Fadenbaum, zu dem die Arbeit gehört, dieselbe
Einheit, nach der nxc status gruppiert, und nicht nur der Zweig, in dem der exklusive Schritt
zufällig begann. Der Anspruch entsteht weiterhin beim ersten exklusiven Schritt, eine Kette, die nie
einen erreicht, hält also nichts; ist er einmal genommen, wird er zurückgegeben, wenn die Operation
fertig ist. Genau das nimmt der zweiten Coding-Runde eines Planers die Lücke davor. Soll die Kopie ab
dem ersten Zug der Operation gehalten werden — also auch während der Planung —, deklarieren Sie
working_tree: exclusive an dem Kanal, der sie eröffnet.
So sieht es aus, wenn eine Runde hinter einem Gespräch geparkt ist, das die Arbeitskopie bereits hält:
$ NXC_ACTOR=alice nxc threads list
m-00000000000000000000000003 decl:build-and-ship 0/1 in [waiting] · waiting for working tree (#1)
m-00000000000000000000000004 decl:build-and-ship 0/1 in [waiting] · waiting for working tree (#1)
m-00000000000000000000000001 dm:9445dbc93dfdad401585c42b 1/1 in [complete]
m-00000000000000000000000002 dm:9445dbc93dfdad401585c42b 0/1 in [waiting] · holding working tree
Beachten Sie, was die Sende-Quittung nicht gesagt hat: ein Kanal-Send meldet spawned: true, auch
wenn jedes Mitglied geparkt ist, denn es ist eine Quittung für die ganze Auffächerung. Die Tafel oben
ist der Ort, an dem ein Anfragender es erfährt. Volle Mechanik unter
Grenzen und Sicherheit.
Vorbedingungen: was gelten muss, bevor ein Schritt startet
- name: coding
members: [coder, finisher]
flow: sequential
working_tree: exclusive
preconditions:
- name: remote-nicht-voraus
run: git rev-list --count HEAD..@{u}
expect: "0"
- name: baum-sauber
run: git status --porcelain
expect: ""Eine Vorbedingung ist eine Hürde, die der Betreuer vor jeden Schritt dieses Kanals stellt. Sie läuft in der Arbeitskopie, in der auch die Sitzungen laufen, und zwar bevor die Sitzung des Schritts gestartet wird — nach dem Start ist die Sitzung da, und was sie vorhatte, ist es auch.
Eine Hürde ist genommen, wenn ihr Befehl mit Null endet — und, wenn die Deklaration ein
expect: nennt, wenn seine getrimmte Standardausgabe genau das ist. Lass expect: weg für einen
Befehl, der schon selbst ein Prädikat ist (test ! -f .nxs/build.lock); schreib expect: "" für
einen, der nichts ausgeben darf. Der Rückgabewert wird immer geprüft, also wird ein Befehl, der
scheitert und zufällig die erwartete Zeichenkette ausgibt, trotzdem abgelehnt: git rev-list --count HEAD..@{u} scheitert ohne konfiguriertes Upstream und gibt nichts aus, und das als „null
Commits zurück" zu lesen wäre genau die falsche Antwort.
Alles Übrige lehnt ebenfalls ab. Ein Befehl, den es nicht gibt, der abstürzt oder der nicht innerhalb seiner Frist fertig wird, hält den Schritt an — und eine Hürde, die in ihre Frist läuft, wird mitsamt allem getötet, was sie gestartet hat (sie läuft in einer eigenen Prozessgruppe), damit nichts von ihr in der Arbeitskopie zurückbleibt, die der nächste Schritt haben will. Eine Hürde, die bei Zweifel durchlässt, ist keine — und eine Laufzeit, die überhaupt keine Befehle ausführen kann, lehnt jede deklarierte Hürde ab, statt sie zu überspringen. Dieselbe Regel, von der anderen Seite gesehen.
Die Hürden werden in ihrer Deklarationsreihenfolge gefragt, und die erste Absage gewinnt —
danach wird nichts mehr ausgeführt, und was zurückkommt, nennt genau eine Hürde und zeigt ihre
Ausgabe. Wo die Absage auftaucht, hängt davon ab, welchen Schritt sie angehalten hat: ein nxc send --to <kanal>, dessen erster Schritt nicht öffnen kann, scheitert unmittelbar und nennt die Hürde;
ein späterer Schritt im Ablauf wird in den warnings derjenigen Antwort gemeldet, die ihn gestartet
hätte — unter der Klasse precondition_refused, ausdrücklich verschieden von step_skipped, das
heißt: etwas ist kaputt, nicht eine Regel hat Nein gesagt. Einen abgelehnten Schritt wiederholt
nichts von selbst: eine Hürde lehnt ab, weil die Welt noch nicht so weit ist, und noch einmal zu
fragen ist Dein Zug.
Was Du nicht deklarieren musst. Manches gilt, ob Du etwas schreibst oder nicht, weil es eine Eigenschaft des Bausteins ist und keine Deines Projekts — vor allem, dass keine Sitzung eines früheren Schritts desselben Kanals noch läuft, wenn der nächste starten soll, auf einem Kanal, der die Arbeitskopie beansprucht. Das ist hier bewusst nicht ausdrückbar: eine Regel, an die man denken muss, ist eine Regel, die die Hälfte der Arbeitsbereiche nicht hat.
Was es kostet, ausgesprochen: ein Prozessstart je deklarierter Hürde je Schritt. Ein Quorum aus vier Prüfern mit zwei Hürden sind acht zusätzliche Prozesse je Runde. Halte Hürden bei schnellen, nur lesenden Fragen; sie laufen innerhalb des Aufrufs, der den Schritt gestartet hätte, und solange sie laufen, kann niemand sonst den Arbeitsbereich benutzen.
Deshalb teilen sich die Hürden eines Schritts EIN Zeitbudget — eine Minute, alle zusammen. Es wird vor jeder Hürde geprüft, eine Hürde wird also nie mehr gestartet, wenn das Budget verbraucht ist; sie kommt als abgelehnt zurück und sagt, dass die vor ihr es aufgebraucht haben. Genau deshalb gibt es keine Obergrenze für die Zahl der Hürden: zu begrenzen war nie die Anzahl, sondern die Zeit.
Und sie sind mit dem Rest der Deklaration eingefroren. Eine Hürde ist ausführbarer Code in einer
Datei, die die deklarierten Agenten selbst ändern dürfen — deshalb prüft ein Vorgang gegen die
Deklarationen, unter denen er geöffnet wurde: preconditions: mitten in einer laufenden Kette zu
ändern, schaltet sie für diese Kette nicht ab, und die Änderung wirkt im nächsten Vorgang. Das ist
ebenso eine Grenze wie eine Zusage — wer zwischen zwei Vorgänge schlüpft, wird davon nicht gefasst.
Vordertüren
- name: front-desk
kind: public
members: [triage]kind: ist group (die Vorgabe) oder public. Ein öffentlicher Kanal ist die Vordertür des
Projekts: seine Nachrichten und Tafeln sind ohne Mitgliedschaft lesbar, und er ist über Workspaces
hinweg auffindbar, die sich einen Sync-Stream teilen. Es ist eine Leseöffnung und sonst nichts —
der Lesezeiger und die mitgliedschaftsbezogenen Auflistungen bleiben Mitgliedern vorbehalten, und
Schreiben war für keine Art je mitgliedschaftsgeprüft.
direct ist bewusst keine deklarierbare Art: ein direktes Gespräch wird aus zwei Handles abgeleitet
und für Sie geprägt, es zu deklarieren wäre also nichts, was man erfüllen könnte — und ließe man das
Wort durch, erzeugte ein Tippfehler still einen Gruppenkanal.
Was aus nxc workflow wurde
Es gab eine Verbgruppe workflow — start, step, status, tick, done, bind, append,
show, expect — mit einem Lauf-Datensatz dahinter. Das alles ist weg, und diese Seite ist ihr
Ersatz, nicht ihre Dokumentation.
Der Grund ist ein Satz aus dem Entwurf: Kanal = Arbeitsablauf. Ein „echter" Workflow ist derselbe
Begriff mit fester Reihenfolge und definierter Schreibberechtigung. Es gibt keinen zweiten Begriff
dafür. Eine feste Reihenfolge aus einem Rollenschritt gefolgt von einem Kanalschritt — die Form, die
jeder deklarierte Workflow tatsächlich hatte — ist flow: sequential. Und „definierte
Schreibberechtigung" brauchte gar kein Feld: jeder Faden hat genau zwei Enden, nur der Öffner eines
Fadens darf dessen Erwartung neu deklarieren, und die Betreuer-Identität ist als Klasse reserviert.
Wer was wo schreiben darf, klärt damit das Datenmodell statt einer Deklaration, die ihm widersprechen
könnte.
Die Zuordnung lautet also:
| War | Ist |
|---|---|
workflow start | nxc send --to <deklarierter Kanal> |
workflow step done | nxc reply --thread <platz> — der Betreuer entscheidet, was folgt |
workflow status / list | nxc status --thread <id> oder blankes nxc status |
workflow expect | das deklarierte expects: des Kanals |
workflow tickets add | --ref nxf_ids=<id> am send, das die Runde eröffnet |
Weiter
- Personas — die Mitglieder, die diese Kanäle benennen.
- Befehle — die Verben, die sie ansprechen.
- Grenzen und Sicherheit — Tiefe, Ansprüche und das menschliche Tor.