faberun 0.15.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -104,6 +104,11 @@ export function candidateRefName(runId) {
104
104
  return `refs/faberun/${runId}/candidate`;
105
105
  }
106
106
 
107
+ /** @param {string} runId @param {string} nodeId @returns {string} */
108
+ export function preservedRefName(runId, nodeId) {
109
+ return `refs/faberun/${runId}/preserved/${nodeId}`;
110
+ }
111
+
107
112
  /** @param {string} runId @param {string} nodeId @param {number} attempt @returns {string} */
108
113
  function attemptBranchName(runId, nodeId, attempt) {
109
114
  return `faberun/${runId}/${nodeId}/${attempt}`;
@@ -168,6 +173,29 @@ export function createRunRef(repo, runId, head) {
168
173
  return ref;
169
174
  }
170
175
 
176
+ /**
177
+ * Keep one node's integrated commit reachable after cancel releases the run
178
+ * ref and the attempt branches: without a ref of its own the commit is
179
+ * garbage the next `git gc` collects, and the persisted snapshot alone cannot
180
+ * bring a pruned object back.
181
+ *
182
+ * Idempotent by the same contract `deleteRef` and `removeWorktree` honour:
183
+ * cancel can legitimately run twice (a retry after a partial first pass, or
184
+ * the operator repeating it), so a preserved ref that already names this
185
+ * exact commit is done, not a conflict — the second creation neither fails
186
+ * nor moves the target. A ref left at a different sha moves to the commit
187
+ * handed in now.
188
+ *
189
+ * @param {string} repo @param {string} runId @param {string} nodeId @param {string|null|undefined} sha @returns {string}
190
+ */
191
+ export function createPreservedRef(repo, runId, nodeId, sha) {
192
+ if (!sha) throw Object.assign(new Error("a preserved ref needs an integrated commit"), { code: "git_head_required" });
193
+ const ref = preservedRefName(runId, nodeId);
194
+ if (gitHead(repo, ref) === sha) return ref;
195
+ runGit(["-C", repo, "update-ref", ref, sha]);
196
+ return ref;
197
+ }
198
+
171
199
  /**
172
200
  * @param {{repo: string, runDir: string, runId: string, nodeId: string, attempt: number, base?: string, declaredReads?: string[]}} args
173
201
  * `base` cuts the new branch from a sealed sha instead of the run ref tip —
@@ -0,0 +1,164 @@
1
+ /**
2
+ * The language of a notification, and the labels it is written in.
3
+ *
4
+ * The controller never sees the operator's conversation with a harness; it
5
+ * sees what the operator wrote for faberun: the campaign goal, the journal
6
+ * notes and the objectives the plan gave each node. Those are the operator's
7
+ * prompts to this product, and a message that answers them in the same
8
+ * language reads as a reply rather than a log line. Detection is a count of
9
+ * function words in the two languages this product's operators write in;
10
+ * anything else, or nothing to read, is English. Quoted content -- the
11
+ * worker's own words, a judge finding -- keeps the language it was written
12
+ * in, because translating a quotation would make it stop being one.
13
+ *
14
+ * It is a module apart from `message.mjs` so the layout and the wording can
15
+ * change independently: a new label is one line here, a new line is one
16
+ * function there.
17
+ */
18
+
19
+ /** @typedef {"en"|"pt"} Language */
20
+
21
+ // `a`, `do` and `no` are left out on purpose: each is also an English word,
22
+ // and one article in an English sentence must not read as Portuguese.
23
+ const PORTUGUESE = new Set(["o", "e", "de", "da", "na", "que", "não", "nao", "para", "com", "uma", "um", "em", "os", "as", "dos", "das", "é", "se", "por", "mais", "como", "mas", "ao", "ou", "quando", "já", "também", "ser", "sem", "até", "cada", "nunca", "está", "são", "foi", "pelo", "pela", "nos", "nas", "seu", "sua", "isso", "esse", "essa", "ele", "ela"]);
24
+ const ENGLISH = new Set(["the", "and", "of", "to", "in", "is", "for", "with", "that", "on", "as", "are", "this", "it", "be", "by", "from", "or", "an", "not", "at", "when", "every", "never", "before", "after", "its", "into", "than", "which", "while", "each", "was", "has", "have"]);
25
+
26
+ /**
27
+ * The language of the first group of samples that carries any function word
28
+ * at all, English when none does. Groups are passed in order of authorship:
29
+ * the campaign goal is the operator's own sentence, the journal notes are the
30
+ * orchestrating session's, the node objectives are the planner's -- and a
31
+ * plan written by a model in English must not outvote the person who typed
32
+ * the goal in Portuguese (measured 2026-09-21: two node objectives held 40
33
+ * English function words against a Portuguese goal's 19).
34
+ *
35
+ * @param {...readonly (string|null|undefined)[]} groups
36
+ * @returns {Language}
37
+ */
38
+ export function detectLanguage(...groups) {
39
+ for (const samples of groups) {
40
+ let portuguese = 0;
41
+ let english = 0;
42
+ for (const sample of samples) {
43
+ if (typeof sample !== "string") continue;
44
+ for (const word of sample.toLowerCase().split(/[^\p{L}]+/u)) {
45
+ if (PORTUGUESE.has(word)) portuguese += 1;
46
+ else if (ENGLISH.has(word)) english += 1;
47
+ }
48
+ }
49
+ if (portuguese + english > 0) return portuguese > english ? "pt" : "en";
50
+ }
51
+ return "en";
52
+ }
53
+
54
+ /**
55
+ * Every phrase the message renders, per language. Node ids, run ids, model
56
+ * names and quoted text are never here: they are data, not wording.
57
+ *
58
+ * @type {Record<Language, Record<string, string>>}
59
+ */
60
+ const LABELS = {
61
+ en: {
62
+ doneIn: "done in",
63
+ running: "running",
64
+ pending: "pending",
65
+ state: "state",
66
+ nothingDelivered: "nothing delivered",
67
+ "no-op": "no-op after",
68
+ failedAfter: "failed after",
69
+ blockedAfter: "blocked after",
70
+ exhaustedAfter: "exhausted after",
71
+ stalledAfter: "stalled after",
72
+ canceledAfter: "canceled after",
73
+ needsYou: "needs you",
74
+ attempt: "attempt",
75
+ run: "run",
76
+ done: "done",
77
+ asked: "asked",
78
+ did: "done",
79
+ proof: "proof",
80
+ delivered: "delivered",
81
+ why: "why",
82
+ do: "do",
83
+ judge: "judge",
84
+ missing: "missing",
85
+ error: "error",
86
+ checksGreen: "checks green",
87
+ checksOf: "checks passed",
88
+ judgePass: "judge pass",
89
+ judgePasses: "judge passes",
90
+ judgeRejected: "judge rejected",
91
+ noJudge: "no judge",
92
+ revisions: "revisions",
93
+ nodes: "nodes",
94
+ next: "next",
95
+ waitingOnYou: "waiting on you",
96
+ phases: "phases",
97
+ cache: "cache",
98
+ in: "in",
99
+ out: "out",
100
+ nextCommand: "next",
101
+ available: "available",
102
+ runUpdate: "run",
103
+ noSummary: "(no summary recorded)",
104
+ noObjective: "(no objective recorded)",
105
+ more: "more",
106
+ complete: "complete",
107
+ },
108
+ pt: {
109
+ doneIn: "concluído em",
110
+ running: "rodando",
111
+ pending: "pendente",
112
+ state: "estado",
113
+ nothingDelivered: "nada entregue",
114
+ "no-op": "sem mudança após",
115
+ failedAfter: "falhou após",
116
+ blockedAfter: "bloqueado após",
117
+ exhaustedAfter: "esgotado após",
118
+ stalledAfter: "travou após",
119
+ canceledAfter: "cancelado após",
120
+ needsYou: "precisa de você",
121
+ attempt: "tentativa",
122
+ run: "run",
123
+ done: "concluídos",
124
+ asked: "pedido",
125
+ did: "feito",
126
+ proof: "prova",
127
+ delivered: "entregue",
128
+ why: "motivo",
129
+ do: "ação",
130
+ judge: "juiz",
131
+ missing: "faltou",
132
+ error: "erro",
133
+ checksGreen: "checagens verdes",
134
+ checksOf: "checagens passaram",
135
+ judgePass: "juiz aprovou",
136
+ judgePasses: "aprovações do juiz",
137
+ judgeRejected: "juiz rejeitou",
138
+ noJudge: "sem juiz",
139
+ revisions: "revisões",
140
+ nodes: "nós",
141
+ next: "próximo",
142
+ waitingOnYou: "esperando você",
143
+ phases: "fases",
144
+ cache: "cache",
145
+ in: "entrada",
146
+ out: "saída",
147
+ nextCommand: "próximo passo",
148
+ available: "disponível",
149
+ runUpdate: "rode",
150
+ noSummary: "(sem resumo registrado)",
151
+ noObjective: "(sem objetivo registrado)",
152
+ more: "mais",
153
+ complete: "completa",
154
+ },
155
+ };
156
+
157
+ /**
158
+ * @param {Language} language
159
+ * @returns {(key: string) => string}
160
+ */
161
+ export function labelsFor(language) {
162
+ const table = LABELS[language];
163
+ return (key) => table[key] ?? LABELS.en[key] ?? key;
164
+ }