strom-research 1.27.0-beta.4 → 1.27.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.
- package/CHANGELOG.cs.md +3 -2
- package/CHANGELOG.de.md +3 -2
- package/CHANGELOG.md +3 -3
- package/CHANGELOG.pl.md +3 -2
- package/dist/commands/session.js +6 -3
- package/dist/core/tree.js +1 -1
- package/dist/runners/codex.js +114 -3
- package/dist/runners/jsonl.js +27 -4
- package/dist/runners/runner.js +40 -3
- package/package.json +1 -4
package/CHANGELOG.cs.md
CHANGED
|
@@ -7,7 +7,7 @@ Změny každé verze ve zkratce. Úplné poznámky a instalační řádky jsou n
|
|
|
7
7
|
|
|
8
8
|
## 1.27.0
|
|
9
9
|
|
|
10
|
-
*
|
|
10
|
+
*5. 11. 2026 · patří k aplikaci Strom 3.23.0.*
|
|
11
11
|
|
|
12
12
|
- **Snímky uložené ručně vrátí svůj úkol samy.** Kde se snímky archivu ukládají ručně – archiv, kam smí jen prohlížeč
|
|
13
13
|
člověka, nebo archiv, jehož podmínky nic jiného nedovolují – výzkum požádá o knihu a čísla snímků a připraví pro ně
|
|
@@ -61,7 +61,8 @@ Změny každé verze ve zkratce. Úplné poznámky a instalační řádky jsou n
|
|
|
61
61
|
`strom help --human` uvádí pořadí úkolů, práci mimo linie, opravy a ponechání rozporu mezi příkazy člověka. Výzkum s
|
|
62
62
|
OpenCode pokračuje i po sezení, které OpenCode řádně uzavřel, ale ohlásil jako neúspěšné, a po přechodné chybě sítě –
|
|
63
63
|
totéž sezení se po krátkém čekání obnoví, v rámci času sezení. Když sandbox Codexu nepustí strom k výzkumu – i v
|
|
64
|
-
rouře a na Windows i u příkazu zadaného přes PowerShell –,
|
|
64
|
+
rouře a na Windows i u příkazu zadaného přes PowerShell –, nebo když příkaz stromu odmítnou už pravidla Codexu, než
|
|
65
|
+
se spustí (na Windows strom v rouře nebo za `;`), výzkum se hned zastaví s tím, co zůstává volbou člověka,
|
|
65
66
|
a takové
|
|
66
67
|
sezení úkol nikdy neodloží. Díky za hlášení na GitHubu. Změnu čtení snímků, kterou výzkum udělal sám a po které se
|
|
67
68
|
četlo zřetelně méně jistě, hned vrátí – už ne až po 60 dnech – a `strom recent` to uvede.
|
package/CHANGELOG.de.md
CHANGED
|
@@ -7,7 +7,7 @@ Die Änderungen jeder Version in Kürze. Die vollständigen Hinweise und die Ins
|
|
|
7
7
|
|
|
8
8
|
## 1.27.0
|
|
9
9
|
|
|
10
|
-
*
|
|
10
|
+
*5.11.2026 · gehört zur Strom-App 3.23.0.*
|
|
11
11
|
|
|
12
12
|
- **Von Hand gespeicherte Bilder bringen ihre Aufgabe von selbst zurück.** Wo die Bilder eines Archivs von Hand
|
|
13
13
|
gespeichert werden – ein Archiv, das nur der Browser eines Menschen erreicht, oder eines, dessen Bedingungen nichts
|
|
@@ -75,7 +75,8 @@ Die Änderungen jeder Version in Kürze. Die vollständigen Hinweise und die Ins
|
|
|
75
75
|
Forschung mit OpenCode geht weiter nach einer Sitzung, die es ordnungsgemäß abgeschlossen, aber als fehlgeschlagen
|
|
76
76
|
gemeldet hat, und nach einem vorübergehenden Netzwerkfehler – dieselbe Sitzung wird nach kurzem Warten fortgesetzt,
|
|
77
77
|
innerhalb der Zeit der Sitzung. Hält die Sandbox von Codex strom von der Forschung fern – auch in einer Pipe und
|
|
78
|
-
unter Windows auch bei einem über PowerShell gegebenen Befehl
|
|
78
|
+
unter Windows auch bei einem über PowerShell gegebenen Befehl – oder lehnen die Regeln von Codex einen strom-Befehl
|
|
79
|
+
ab, bevor er läuft (unter Windows strom in einer Pipe oder nach `;`), hält die Forschung sofort an und nennt, was die Wahl
|
|
79
80
|
des Menschen bleibt; eine
|
|
80
81
|
solche Sitzung legt ihre Aufgabe nie beiseite. Danke für die Meldung auf GitHub. Eine Änderung beim Lesen der Scans,
|
|
81
82
|
die die Forschung selbst vorgenommen hat und nach der die Lesungen deutlich unsicherer wurden, wird sofort
|
package/CHANGELOG.md
CHANGED
|
@@ -7,7 +7,7 @@ The changes of each version in short. The full notes and the install lines are o
|
|
|
7
7
|
|
|
8
8
|
## 1.27.0
|
|
9
9
|
|
|
10
|
-
*
|
|
10
|
+
*2026-11-05 · goes with the Strom app 3.23.0.*
|
|
11
11
|
|
|
12
12
|
- **Images saved by hand bring their task back by themselves.** Where an archive's images are saved by hand – an archive
|
|
13
13
|
only a person's browser reaches, or one whose terms allow nothing else – the research asks for its book and the
|
|
@@ -65,8 +65,8 @@ The changes of each version in short. The full notes and the install lines are o
|
|
|
65
65
|
counted. `strom help --human` lists the order of tasks, work off the lines, fixes and leaving a conflict among the
|
|
66
66
|
person's own commands. OpenCode's research goes on after a session it closed properly but reported as failed, and
|
|
67
67
|
after a passing network error – the same session resumed after a short wait, within the session's time. Codex kept
|
|
68
|
-
away from strom by its sandbox – also in a pipe, and on Windows with a command said through PowerShell –
|
|
69
|
-
research at once with what remains the person's choice, and such a session never puts its task aside. Thanks for the
|
|
68
|
+
away from strom by its sandbox – also in a pipe, and on Windows with a command said through PowerShell – or a strom
|
|
69
|
+
command its own rules refuse before it runs (on Windows strom in a pipe or after `;`) stops the research at once with what remains the person's choice, and such a session never puts its task aside. Thanks for the
|
|
70
70
|
report on GitHub.
|
|
71
71
|
A change the research made by itself in reading the scans, after which the readings got clearly less sure, is taken
|
|
72
72
|
back at once – no longer only after 60 days – and `strom recent` says so.
|
package/CHANGELOG.pl.md
CHANGED
|
@@ -7,7 +7,7 @@ Zmiany każdej wersji w skrócie. Pełne informacje o wydaniach i polecenia inst
|
|
|
7
7
|
|
|
8
8
|
## 1.27.0
|
|
9
9
|
|
|
10
|
-
*
|
|
10
|
+
*5.11.2026 · współpracuje z aplikacją Strom 3.23.0.*
|
|
11
11
|
|
|
12
12
|
- **Skany zapisane ręcznie same przywracają swoje zadanie.** Tam, gdzie skany archiwum zapisuje się ręcznie – archiwum,
|
|
13
13
|
do którego wolno tylko przeglądarce człowieka, albo takie, którego warunki nie pozwalają na nic innego – badanie
|
|
@@ -68,7 +68,8 @@ Zmiany każdej wersji w skrócie. Pełne informacje o wydaniach i polecenia inst
|
|
|
68
68
|
sprzeczności wśród poleceń człowieka. Badanie z OpenCode toczy się dalej po sesji, którą OpenCode poprawnie zamknął,
|
|
69
69
|
ale zgłosił jako nieudaną, oraz po przejściowym błędzie sieci – ta sama sesja zostaje wznowiona po krótkim
|
|
70
70
|
oczekiwaniu, w ramach czasu sesji. Gdy sandbox Codexa nie dopuści strom do badań – także w potoku, a w Windows
|
|
71
|
-
także przy poleceniu podanym przez PowerShell
|
|
71
|
+
także przy poleceniu podanym przez PowerShell – albo gdy polecenie strom odrzucą już reguły Codexa, zanim się
|
|
72
|
+
uruchomi (w Windows strom w potoku lub po `;`), badanie od razu się zatrzymuje z tym, co pozostaje wyborem
|
|
72
73
|
człowieka, a taka sesja nigdy
|
|
73
74
|
nie odkłada zadania. Dziękujemy za zgłoszenie na GitHubie. Zmiana odczytu skanów, którą badanie wprowadziło samo i po
|
|
74
75
|
której odczyty stały się wyraźnie mniej pewne, zostaje od razu cofnięta – już nie dopiero po 60 dniach – a `strom
|
package/dist/commands/session.js
CHANGED
|
@@ -1196,8 +1196,11 @@ register(updatedDuringRun({
|
|
|
1196
1196
|
const after = Tree.open(root, runEnv);
|
|
1197
1197
|
let s = after.get(session.id);
|
|
1198
1198
|
// strom itself refused to the agent (its permissions, its sandbox — said through any shell): kept with the
|
|
1199
|
-
// session's metrics, so the session is not counted against its task
|
|
1200
|
-
|
|
1199
|
+
// session's metrics, so the session is not counted against its task. strom first in the command — in any part
|
|
1200
|
+
// of it only where the agent refused the command whole before any of it ran (Codex's policy: deniedWhole); a
|
|
1201
|
+
// sandbox that refused `touch ../x` in `touch ../x && strom …` refused no strom
|
|
1202
|
+
const whole = new Set(result.deniedWhole ?? []);
|
|
1203
|
+
const stromDenied = result.denied?.filter((d, i) => deniedStrom(d, whole.has(i))).length ?? 0;
|
|
1201
1204
|
if (stromDenied)
|
|
1202
1205
|
result = { ...result, metrics: { ...result.metrics, stromDenied } };
|
|
1203
1206
|
// the agent closed the session itself and its process then failed by its exit code alone (OpenCode: 1 after its
|
|
@@ -1253,7 +1256,7 @@ register(updatedDuringRun({
|
|
|
1253
1256
|
// what it cost: the agent's own and its readers', both apart too
|
|
1254
1257
|
const m = after.get(s.id)?.metrics ?? withReaders(result.metrics, s.metrics);
|
|
1255
1258
|
const total = sessionCost(m);
|
|
1256
|
-
report.push({ session: s.id, task: task.id, outcome: result.outcome, ...(result.exitOnly && result.outcome === "ok" ? { agentExit: result.exitCode } : {}), ...(result.recovered ? { recovered: result.recovered } : {}), ...(s.summary ? { summary: s.summary } : {}), ...(total !== undefined ? { costUsd: total } : {}), ...(m.readersUsd !== undefined ? { agentUsd: m.costUsd ?? 0, readersUsd: m.readersUsd } : {}) });
|
|
1259
|
+
report.push({ session: s.id, task: task.id, outcome: result.outcome, ...(result.exitOnly && result.outcome === "ok" ? { agentExit: result.exitCode } : {}), ...(result.recovered ? { recovered: result.recovered } : {}), ...(result.denied?.length ? { denied: result.denied.length } : {}), ...(stromDenied ? { stromDenied } : {}), ...(s.summary ? { summary: s.summary } : {}), ...(total !== undefined ? { costUsd: total } : {}), ...(m.readersUsd !== undefined ? { agentUsd: m.costUsd ?? 0, readersUsd: m.readersUsd } : {}) });
|
|
1257
1260
|
out(`■ ${s.id} ${s.state} · ${result.outcome}${costText(m) ? ` · ${costText(m)}` : ""}${s.summary ? ` · ${truncate(s.summary, 80)}` : ""}`);
|
|
1258
1261
|
if (result.denied?.length)
|
|
1259
1262
|
out(ui(lang, "ui.run.denied", { n: result.denied.length, what: result.denied.slice(0, 3).join(" · ") }));
|
package/dist/core/tree.js
CHANGED
|
@@ -31,7 +31,7 @@ export const TREE_FILE = "strom.json";
|
|
|
31
31
|
export const SEAL_FOREIGN = "seal.foreign";
|
|
32
32
|
// between releases the candidate being made (a bridge run from the sources says what it is: the Strom app goes by it);
|
|
33
33
|
// at a release the release's own (package.json)
|
|
34
|
-
export const VERSION = "1.27.0
|
|
34
|
+
export const VERSION = "1.27.0";
|
|
35
35
|
const GITIGNORE = `# Strom Research — local state and large files are not versioned
|
|
36
36
|
.strom/
|
|
37
37
|
/media/
|
package/dist/runners/codex.js
CHANGED
|
@@ -135,6 +135,105 @@ export function codexRefusedStrom(output, session) {
|
|
|
135
135
|
const mark = refusalMark(session);
|
|
136
136
|
return !!mark && output.replace(/\r?\n/g, "").includes(`${SANDBOX_REFUSAL} ${mark}`);
|
|
137
137
|
}
|
|
138
|
+
/**
|
|
139
|
+
* One string as Rust's Debug writes it (`"…"` with `\"`, `\\`, `\n`, `\u{…}`), read from its opening quote at `at`:
|
|
140
|
+
* its text and where it ended — undefined when it does not end (a line cut short).
|
|
141
|
+
*/
|
|
142
|
+
function debugString(s, at) {
|
|
143
|
+
if (s[at] !== '"')
|
|
144
|
+
return undefined;
|
|
145
|
+
let text = "";
|
|
146
|
+
for (let i = at + 1; i < s.length; i++) {
|
|
147
|
+
const c = s[i];
|
|
148
|
+
if (c === '"')
|
|
149
|
+
return { text, end: i + 1 };
|
|
150
|
+
if (c !== "\\") {
|
|
151
|
+
text += c;
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
const e = s[++i];
|
|
155
|
+
if (e === undefined)
|
|
156
|
+
return undefined;
|
|
157
|
+
if (e === "u") {
|
|
158
|
+
const u = /^\{([0-9a-fA-F]{1,6})\}/u.exec(s.slice(i + 1));
|
|
159
|
+
if (!u)
|
|
160
|
+
return undefined;
|
|
161
|
+
text += String.fromCodePoint(parseInt(u[1], 16));
|
|
162
|
+
i += u[0].length;
|
|
163
|
+
}
|
|
164
|
+
else
|
|
165
|
+
text += { n: "\n", r: "\r", t: "\t", "0": "\0" }[e] ?? e;
|
|
166
|
+
}
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* A command line as Codex renders it (the shlex crate's join: words '…'-quoted, or "…"-quoted with \ before $ ` " \
|
|
171
|
+
* where a backslash keeps single quotes out — Windows paths) split into its words again.
|
|
172
|
+
*/
|
|
173
|
+
function shlexWords(line) {
|
|
174
|
+
const words = [];
|
|
175
|
+
let word;
|
|
176
|
+
for (let i = 0; i < line.length; i++) {
|
|
177
|
+
const c = line[i];
|
|
178
|
+
if (/\s/u.test(c)) {
|
|
179
|
+
if (word !== undefined)
|
|
180
|
+
words.push(word);
|
|
181
|
+
word = undefined;
|
|
182
|
+
}
|
|
183
|
+
else if (c === "'") {
|
|
184
|
+
const close = line.indexOf("'", i + 1);
|
|
185
|
+
if (close < 0)
|
|
186
|
+
return [];
|
|
187
|
+
word = (word ?? "") + line.slice(i + 1, close);
|
|
188
|
+
i = close;
|
|
189
|
+
}
|
|
190
|
+
else if (c === '"') {
|
|
191
|
+
word ??= "";
|
|
192
|
+
for (i++; i < line.length && line[i] !== '"'; i++)
|
|
193
|
+
word += line[i] === "\\" && i + 1 < line.length ? line[++i] : line[i];
|
|
194
|
+
if (i >= line.length)
|
|
195
|
+
return [];
|
|
196
|
+
}
|
|
197
|
+
else
|
|
198
|
+
word = (word ?? "") + (c === "\\" && i + 1 < line.length ? line[++i] : c);
|
|
199
|
+
}
|
|
200
|
+
if (word !== undefined)
|
|
201
|
+
words.push(word);
|
|
202
|
+
return words;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* A command Codex's own policy refused before it ran, as Codex alone says it — a line of its log on its stderr, never a
|
|
206
|
+
* command_execution item, never what a command printed or the agent wrote (codex-cli 0.166.0 on Windows, found
|
|
207
|
+
* 2026-11-05):
|
|
208
|
+
* `<time> ERROR codex_core::tools::router: error=exec_command failed: CreateProcess { message: "Rejected(\"`<the
|
|
209
|
+
* command, its shell around it>` rejected: blocked by policy\")" }`
|
|
210
|
+
* "Blocked by policy" is an approval nobody gives in `codex exec`: with Codex's own Windows sandbox not in use (as where
|
|
211
|
+
* it was found) every part of a command no rule allows is refused — `strom.cmd … | Select-Object`, `Set-Location .; strom.cmd …`:
|
|
212
|
+
* the rule lets strom run, the part after it is refused, and so the whole command (Codex's exec_policy.rs,
|
|
213
|
+
* render_decision_for_unmatched_command). A refusal by a rule of its own (strom's `git`, `strom login`, the person's:
|
|
214
|
+
* its justification) or of Codex's `rm -f` is what the rules say: no such refusal here. The command as the agent meant
|
|
215
|
+
* it (its shell around it taken off); undefined: no such line.
|
|
216
|
+
*/
|
|
217
|
+
export function codexPolicyRejected(line) {
|
|
218
|
+
const clean = line.replace(/\u001b\[[0-9;]*m/gu, "");
|
|
219
|
+
const at = /^\s*(?:\S+\s+)?ERROR\s+codex_core::tools::router:\s+error=[\p{L}\p{N}_.-]+ failed: CreateProcess \{ message: "/u.exec(clean);
|
|
220
|
+
if (!at)
|
|
221
|
+
return undefined;
|
|
222
|
+
const outer = debugString(clean, at.index + at[0].length - 1);
|
|
223
|
+
if (!outer || !/^\s*\}\s*$/u.test(clean.slice(outer.end)) || !outer.text.startsWith('Rejected("'))
|
|
224
|
+
return undefined;
|
|
225
|
+
const reason = debugString(outer.text, "Rejected(".length);
|
|
226
|
+
if (!reason || outer.text.slice(reason.end) !== ")")
|
|
227
|
+
return undefined;
|
|
228
|
+
const said = /^`([\s\S]+)` rejected: blocked by policy$/u.exec(reason.text);
|
|
229
|
+
if (!said)
|
|
230
|
+
return undefined;
|
|
231
|
+
const words = shlexWords(said[1]);
|
|
232
|
+
// the agent's own command: what its shell was given (-Command, -c, /c, -lc), else the words as they are
|
|
233
|
+
const flag = words.findIndex((w, i) => i > 0 && /^(?:-(?:command|c|lc)|\/[ck])$/iu.test(w));
|
|
234
|
+
const command = flag > 0 && flag < words.length - 1 ? words.slice(flag + 1).join(" ") : words.length ? words.join(" ") : said[1];
|
|
235
|
+
return shellUnwrapped(command);
|
|
236
|
+
}
|
|
138
237
|
/** The error Codex said last, kept when an earlier one said the plan's limit (the later one may only say the turn failed). */
|
|
139
238
|
function failure(said, had) {
|
|
140
239
|
return looksLikeLimit(had).limit && !looksLikeLimit(said).limit ? had : said || had;
|
|
@@ -165,6 +264,17 @@ function runCodex(opts) {
|
|
|
165
264
|
tellUsage(opts, { total: true, in: usageNumber(u.input_tokens), cr: usageNumber(u.cached_input_tokens), out: usageNumber(u.output_tokens), turns: steps || undefined });
|
|
166
265
|
steps = 0;
|
|
167
266
|
};
|
|
267
|
+
// a command its policy refused before it ran is said on its stderr alone (codexPolicyRejected): counted like a denial —
|
|
268
|
+
// strom's own command so refused stops the run as its sandbox's refusal does (#8: on Windows Codex runs every command
|
|
269
|
+
// through PowerShell, and strom in a pipe, after `;` or `&` was refused so — the run went on and put its tasks aside)
|
|
270
|
+
const stderrLine = (line, heard) => {
|
|
271
|
+
const command = codexPolicyRejected(line);
|
|
272
|
+
if (command === undefined)
|
|
273
|
+
return;
|
|
274
|
+
// refused whole before any of it ran: strom counts refused in any part of it (deniedStrom's `whole`)
|
|
275
|
+
(heard.deniedWhole ??= []).push(heard.denied.length);
|
|
276
|
+
heard.denied.push(`Bash: ${command.slice(0, 120)}`);
|
|
277
|
+
};
|
|
168
278
|
return runJsonLines("codex", codexArgs(opts), opts.env, opts, resume, (msg, heard) => {
|
|
169
279
|
if (msg.type === "thread.started" && typeof msg.thread_id === "string") {
|
|
170
280
|
heard.sessionId = msg.thread_id;
|
|
@@ -185,8 +295,9 @@ function runCodex(opts) {
|
|
|
185
295
|
// "declined"): counted like a denial, the command as the agent meant it (its shell around it taken off). strom's own
|
|
186
296
|
// refusal under an agent counts with the exit code 0 too: in a pipe (`strom … | cat`, `| Select-Object`) the code
|
|
187
297
|
// is the last program's — but then only strom's error with this session's mark, never its words quoted
|
|
188
|
-
// (codexRefusedStrom). (Codex
|
|
189
|
-
//
|
|
298
|
+
// (codexRefusedStrom). (Codex's exec_command gives no item of a command its policy refused before it ran — said on
|
|
299
|
+
// its stderr alone: stderrLine above — nor of one its sandbox stopped within 150 ms; strom itself starts slower and
|
|
300
|
+
// is always said.)
|
|
190
301
|
if (msg.type === "item.completed" &&
|
|
191
302
|
item?.type === "command_execution" &&
|
|
192
303
|
(item.status === "declined" ||
|
|
@@ -218,5 +329,5 @@ function runCodex(opts) {
|
|
|
218
329
|
const e = msg.error;
|
|
219
330
|
heard.text = failure(String(e?.message ?? msg.message ?? ""), heard.text);
|
|
220
331
|
}
|
|
221
|
-
});
|
|
332
|
+
}, { stderrLine });
|
|
222
333
|
}
|
package/dist/runners/jsonl.js
CHANGED
|
@@ -78,7 +78,7 @@ export async function runJsonLines(command, args, env, opts, resume, onEvent, wa
|
|
|
78
78
|
heard.deniedBefore = heard.denied.length;
|
|
79
79
|
heard.error = undefined;
|
|
80
80
|
heard.endedNormally = undefined;
|
|
81
|
-
const a = await attempt(command, args, input, env, opts, timeoutMs, heard, onEvent);
|
|
81
|
+
const a = await attempt(command, args, input, env, opts, timeoutMs, heard, onEvent, ways.stderrLine);
|
|
82
82
|
lastStderr = a.stderr;
|
|
83
83
|
// (its events set them while it ran)
|
|
84
84
|
const said = heard.responses;
|
|
@@ -200,12 +200,13 @@ export async function runJsonLines(command, args, env, opts, resume, onEvent, wa
|
|
|
200
200
|
...(recovered ? { recovered } : {}),
|
|
201
201
|
...(limit.resumeAt ? { resumeAt: limit.resumeAt } : {}),
|
|
202
202
|
...(heard.denied.length ? { denied: heard.denied } : {}),
|
|
203
|
+
...(heard.deniedWhole?.length ? { deniedWhole: heard.deniedWhole } : {}),
|
|
203
204
|
...(heard.refused?.length ? { refused: heard.refused } : {}),
|
|
204
205
|
...(outcome === "error" && looksLikeModelRejected(all) ? { modelRejected: true } : {}),
|
|
205
206
|
};
|
|
206
207
|
}
|
|
207
208
|
/** One start of the agent; its events go to `heard`. */
|
|
208
|
-
function attempt(command, args, input, env, opts, timeoutMs, heard, onEvent) {
|
|
209
|
+
function attempt(command, args, input, env, opts, timeoutMs, heard, onEvent, onStderr) {
|
|
209
210
|
return new Promise((resolve) => {
|
|
210
211
|
const child = spawnAgent(command, args, { cwd: opts.cwd, env, stdio: ["pipe", "pipe", "pipe"], windowsHide: true, detached: OWN_GROUP });
|
|
211
212
|
feedStdin(child, input);
|
|
@@ -246,9 +247,28 @@ function attempt(command, args, input, env, opts, timeoutMs, heard, onEvent) {
|
|
|
246
247
|
log.flush();
|
|
247
248
|
});
|
|
248
249
|
let stderr = "";
|
|
250
|
+
// its stderr a whole line at a time too, for a runner that reads it (a line cut between two chunks read whole)
|
|
251
|
+
const errText = new StringDecoder("utf8");
|
|
252
|
+
let errLine = "";
|
|
253
|
+
const errLines = (chunk, end = false) => {
|
|
254
|
+
if (!onStderr)
|
|
255
|
+
return;
|
|
256
|
+
errLine += chunk;
|
|
257
|
+
let nl;
|
|
258
|
+
while ((nl = errLine.indexOf("\n")) >= 0) {
|
|
259
|
+
onStderr(errLine.slice(0, nl).replace(/\r$/u, ""), heard);
|
|
260
|
+
errLine = errLine.slice(nl + 1);
|
|
261
|
+
}
|
|
262
|
+
if (end && errLine)
|
|
263
|
+
onStderr(errLine.replace(/\r$/u, ""), heard);
|
|
264
|
+
if (end)
|
|
265
|
+
errLine = "";
|
|
266
|
+
};
|
|
249
267
|
child.stderr?.on("data", (d) => {
|
|
250
|
-
|
|
251
|
-
|
|
268
|
+
const t = errText.write(d);
|
|
269
|
+
stderr += t;
|
|
270
|
+
appendLog(opts.logFile, t);
|
|
271
|
+
errLines(t);
|
|
252
272
|
});
|
|
253
273
|
const end = () => {
|
|
254
274
|
if (timer)
|
|
@@ -264,6 +284,9 @@ function attempt(command, args, input, env, opts, timeoutMs, heard, onEvent) {
|
|
|
264
284
|
buffer += text.end();
|
|
265
285
|
if (buffer)
|
|
266
286
|
handle(buffer);
|
|
287
|
+
const errRest = errText.end();
|
|
288
|
+
stderr += errRest;
|
|
289
|
+
errLines(errRest, true);
|
|
267
290
|
log.rest(buffer);
|
|
268
291
|
log.flush();
|
|
269
292
|
resolve({ code, signal, timedOut, stderr });
|
package/dist/runners/runner.js
CHANGED
|
@@ -79,11 +79,48 @@ export function shellUnwrapped(command) {
|
|
|
79
79
|
}
|
|
80
80
|
/**
|
|
81
81
|
* Whether a refused tool call ("Bash: …", "PowerShell: …", as a run collects them) ran strom itself: strom, strom.cmd,
|
|
82
|
-
* strom.exe, strom.ps1 or a second installation's strom-<suffix>, by its name or its path, first in the command
|
|
82
|
+
* strom.exe, strom.ps1 or a second installation's strom-<suffix>, by its name or its path, first in the command — never
|
|
83
|
+
* inside quotes (`echo "x; strom brief"`) or as another program's argument. `whole`: the command was refused whole
|
|
84
|
+
* before any of it ran (Codex's policy, RunResult.deniedWhole): there strom counts first in any part of it too — after
|
|
85
|
+
* `;`, `&&`, `||`, a pipe or a new line (`Set-Location .; strom.cmd …`: found on Windows, #8). A command refused while
|
|
86
|
+
* it ran (a sandbox) is strom's only with strom first: in `touch ../x && strom …` the sandbox refused touch, and strom
|
|
87
|
+
* never ran.
|
|
83
88
|
*/
|
|
84
|
-
export function deniedStrom(denied) {
|
|
89
|
+
export function deniedStrom(denied, whole = false) {
|
|
85
90
|
const command = shellUnwrapped(denied.replace(/^[\p{L}\p{N}_]+: /u, ""));
|
|
86
|
-
return
|
|
91
|
+
return (whole ? commandParts(command) : [command]).some((part) => STROM_FIRST.test(part.replace(/^&\s*/u, "")));
|
|
92
|
+
}
|
|
93
|
+
/** strom by its name or path first in a command (its own quotes around it, the same at both ends). */
|
|
94
|
+
const STROM_FIRST = /^(['"]?)(?:[^\s'"]*[\\/])?strom(?:-[\p{L}\p{N}_.-]+?)?(?:\.cmd|\.exe|\.ps1|\.bat)?\1(?:\s|$)/iu;
|
|
95
|
+
/** The parts of a command line between `;`, `&&`, `||`, `|` and new lines outside quotes, each trimmed. */
|
|
96
|
+
function commandParts(command) {
|
|
97
|
+
const parts = [];
|
|
98
|
+
let part = "";
|
|
99
|
+
let quote;
|
|
100
|
+
for (let i = 0; i < command.length; i++) {
|
|
101
|
+
const c = command[i];
|
|
102
|
+
if (quote) {
|
|
103
|
+
part += c;
|
|
104
|
+
if (c === "\\" && quote === '"' && i + 1 < command.length)
|
|
105
|
+
part += command[++i];
|
|
106
|
+
else if (c === quote)
|
|
107
|
+
quote = undefined;
|
|
108
|
+
}
|
|
109
|
+
else if (c === "'" || c === '"') {
|
|
110
|
+
quote = c;
|
|
111
|
+
part += c;
|
|
112
|
+
}
|
|
113
|
+
else if (c === ";" || c === "\n" || c === "|" || (c === "&" && command[i + 1] === "&")) {
|
|
114
|
+
parts.push(part.trim());
|
|
115
|
+
part = "";
|
|
116
|
+
if (command[i + 1] === c)
|
|
117
|
+
i++;
|
|
118
|
+
}
|
|
119
|
+
else
|
|
120
|
+
part += c;
|
|
121
|
+
}
|
|
122
|
+
parts.push(part.trim());
|
|
123
|
+
return parts.filter(Boolean);
|
|
87
124
|
}
|
|
88
125
|
/** A command as a run's progress shows it: "$ " and its first line, cut at 140 characters — a cut ends in "…". */
|
|
89
126
|
export function commandLine(command) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "strom-research",
|
|
3
|
-
"version": "1.27.0
|
|
3
|
+
"version": "1.27.0",
|
|
4
4
|
"description": "Genealogical research toolkit for AI coding agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"stromLayout": 2,
|
|
@@ -59,8 +59,5 @@
|
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"@types/node": "^22.20.4",
|
|
61
61
|
"typescript": "^7.0.2"
|
|
62
|
-
},
|
|
63
|
-
"publishConfig": {
|
|
64
|
-
"tag": "beta"
|
|
65
62
|
}
|
|
66
63
|
}
|