@code-yeongyu/senpi-codemode 2026.10.30-1 → 2026.10.31-1
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.md +27 -0
- package/package.json +4 -4
- package/src/kernels/js/group-signal-notice.js +277 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2026.10.31-1] - 2026-10-31
|
|
4
|
+
|
|
5
|
+
### Breaking Changes
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
### Removed
|
|
14
|
+
|
|
15
|
+
## [2026.10.30-2] - 2026-10-30
|
|
16
|
+
|
|
17
|
+
### Breaking Changes
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- Group-signal notices ignore inert quoted text and shell comments without hiding commands inside executable substitutions ([#3019](https://github.com/code-yeongyu/senpi/issues/3019)).
|
|
26
|
+
- Keep held permission decisions parked until their two-second deadline when the host claims the shared slot before the worker waits (#4709).
|
|
27
|
+
|
|
28
|
+
### Removed
|
|
29
|
+
|
|
3
30
|
## [2026.10.30-1] - 2026-10-30
|
|
4
31
|
|
|
5
32
|
### Breaking Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@code-yeongyu/senpi-codemode",
|
|
3
|
-
"version": "2026.10.
|
|
3
|
+
"version": "2026.10.31-1",
|
|
4
4
|
"description": "Source-only senpi extension package for codemode evaluation tools",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -33,15 +33,15 @@
|
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@babel/parser": "8.0.4",
|
|
36
|
-
"@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.10.
|
|
36
|
+
"@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.10.31-1",
|
|
37
37
|
"quickjs-wasi": "3.6.2",
|
|
38
38
|
"typebox": "1.3.34"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
|
-
"@code-yeongyu/senpi": "2026.10.
|
|
41
|
+
"@code-yeongyu/senpi": "2026.10.31-1"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
|
-
"@code-yeongyu/senpi": "2026.10.
|
|
44
|
+
"@code-yeongyu/senpi": "2026.10.31-1"
|
|
45
45
|
},
|
|
46
46
|
"keywords": [
|
|
47
47
|
"senpi",
|
|
@@ -26,10 +26,286 @@ const PKILL_GROUP = new RegExp(String.raw`${COMMAND_POSITION}pkill\b[^\n;|&]*\s(
|
|
|
26
26
|
// can include the agent's own (bun, node, senpi). Inside an `echo` string it is only text.
|
|
27
27
|
const KILLALL = new RegExp(String.raw`${COMMAND_POSITION}killall\s`, "mu");
|
|
28
28
|
|
|
29
|
+
// senpi#3019: a keyword inside quoted text is not a command position, so the scan runs on a copy whose inert text is
|
|
30
|
+
// blanked: single-quoted strings, comments, and the literal parts of double-quoted strings and heredoc bodies (all of a
|
|
31
|
+
// body whose delimiter is quoted). A command substitution, `$(...)` or a backtick pair, runs even inside double quotes
|
|
32
|
+
// and unquoted heredoc bodies, so it keeps its text, as a `-c` script does; both are command lines, with their own inert
|
|
33
|
+
// text blanked in turn. A `<<` whose delimiter line never comes blanks nothing.
|
|
34
|
+
const SCRIPT_FLAG_BEFORE_QUOTE = /(?:^|\s)-[A-Za-z]*c\s+$/u;
|
|
35
|
+
const WORD_END = /[\s;&|<>()]/u;
|
|
36
|
+
const BEFORE_COMMENT = /[\s;&|()]/u;
|
|
37
|
+
const MAX_SCAN_WORK = 262_144;
|
|
38
|
+
const SCAN_LIMIT = Symbol("advisory scan limit");
|
|
39
|
+
|
|
40
|
+
function scanStep(scan, nesting, work = 1) {
|
|
41
|
+
scan.remaining -= work;
|
|
42
|
+
if (scan.remaining < 0 || nesting > 64) throw SCAN_LIMIT;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function blank(text) {
|
|
46
|
+
return text.replace(/[^\n]/gu, "x");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// For a `$(` or a backtick at `index`: the index of the `)` or backtick that closes it, or -1 when none does. Anything
|
|
50
|
+
// else: undefined.
|
|
51
|
+
function substitutionEnd(text, index, scan, nesting) {
|
|
52
|
+
if (text[index] === "`") {
|
|
53
|
+
const end = text.indexOf("`", index + 1);
|
|
54
|
+
scanStep(scan, nesting, (end === -1 ? text.length : end) - index);
|
|
55
|
+
return end;
|
|
56
|
+
}
|
|
57
|
+
if (text[index] !== "$" || text[index + 1] !== "(") return undefined;
|
|
58
|
+
let depth = 0;
|
|
59
|
+
const cases = [];
|
|
60
|
+
let commandStart = true;
|
|
61
|
+
for (let at = index + 1; at < text.length; at++) {
|
|
62
|
+
scanStep(scan, nesting);
|
|
63
|
+
const char = text[at];
|
|
64
|
+
const currentCase = cases.at(-1);
|
|
65
|
+
const atCaseDepth = currentCase?.depth === depth;
|
|
66
|
+
if (char === "\\") {
|
|
67
|
+
if (text[at + 1] !== "\n") {
|
|
68
|
+
if (atCaseDepth && currentCase.header === 2) currentCase.header = 1;
|
|
69
|
+
if (atCaseDepth && currentCase.pattern) currentCase.patternStarted = true;
|
|
70
|
+
commandStart = false;
|
|
71
|
+
}
|
|
72
|
+
at++;
|
|
73
|
+
} else if (char === "#" && BEFORE_COMMENT.test(text[at - 1])) {
|
|
74
|
+
const end = text.indexOf("\n", at);
|
|
75
|
+
scanStep(scan, nesting, (end === -1 ? text.length : end) - at);
|
|
76
|
+
if (end === -1) return -1;
|
|
77
|
+
at = end;
|
|
78
|
+
commandStart = true;
|
|
79
|
+
} else if (char === "'" || char === '"' || char === "`") {
|
|
80
|
+
if (atCaseDepth && currentCase.header === 2) currentCase.header = 1;
|
|
81
|
+
if (atCaseDepth && currentCase.pattern) currentCase.patternStarted = true;
|
|
82
|
+
at = closingQuote(text, at, scan, nesting + 1);
|
|
83
|
+
commandStart = false;
|
|
84
|
+
} else if (char === "(") {
|
|
85
|
+
// A case pattern may open with "(", but its mandatory ")" never closes the substitution.
|
|
86
|
+
if (atCaseDepth && currentCase.pattern && !currentCase.patternStarted) currentCase.patternStarted = true;
|
|
87
|
+
else depth++;
|
|
88
|
+
commandStart = !currentCase?.pattern || text[at - 1] === "$";
|
|
89
|
+
} else if (char === ")") {
|
|
90
|
+
if (atCaseDepth && currentCase.pattern) {
|
|
91
|
+
currentCase.pattern = false;
|
|
92
|
+
commandStart = true;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
depth--;
|
|
96
|
+
if (depth === 0) return at;
|
|
97
|
+
commandStart = false;
|
|
98
|
+
} else if (char === ";") {
|
|
99
|
+
const terminator = text.startsWith(";;&", at) ? 3 : text[at + 1] === ";" || text[at + 1] === "&" ? 2 : 0;
|
|
100
|
+
if (atCaseDepth && currentCase.header === 0 && !currentCase.pattern && terminator > 0) {
|
|
101
|
+
currentCase.pattern = true;
|
|
102
|
+
currentCase.patternStarted = false;
|
|
103
|
+
at += terminator - 1;
|
|
104
|
+
}
|
|
105
|
+
commandStart = true;
|
|
106
|
+
} else if (char === "\n" || char === "|" || char === "&") {
|
|
107
|
+
commandStart = true;
|
|
108
|
+
} else if (!WORD_END.test(char)) {
|
|
109
|
+
let end = at + 1;
|
|
110
|
+
while (end < text.length && !WORD_END.test(text[end]) && !["'", '"', "\\", "`"].includes(text[end])) {
|
|
111
|
+
scanStep(scan, nesting);
|
|
112
|
+
end++;
|
|
113
|
+
}
|
|
114
|
+
const word = text.slice(at, end);
|
|
115
|
+
if (atCaseDepth && currentCase.header === 2) {
|
|
116
|
+
currentCase.header = 1;
|
|
117
|
+
commandStart = false;
|
|
118
|
+
} else if (atCaseDepth && currentCase.header === 1 && word === "in") {
|
|
119
|
+
currentCase.header = 0;
|
|
120
|
+
currentCase.pattern = true;
|
|
121
|
+
commandStart = false;
|
|
122
|
+
} else if (atCaseDepth && currentCase.pattern) {
|
|
123
|
+
if (word === "esac" && !currentCase.patternStarted) cases.pop();
|
|
124
|
+
else currentCase.patternStarted = true;
|
|
125
|
+
commandStart = false;
|
|
126
|
+
} else if (word === "case" && commandStart) {
|
|
127
|
+
cases.push({ depth, header: 2, pattern: false, patternStarted: false });
|
|
128
|
+
commandStart = false;
|
|
129
|
+
} else if (word === "esac" && commandStart && atCaseDepth) {
|
|
130
|
+
cases.pop();
|
|
131
|
+
commandStart = false;
|
|
132
|
+
} else {
|
|
133
|
+
commandStart = commandStart && /^(?:if|then|do|else|elif|while|until|!|\{)$/u.test(word);
|
|
134
|
+
}
|
|
135
|
+
at = end - 1;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return -1;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// The index of the quote that closes the one at `open`, or the text length. A substitution inside double quotes is a
|
|
142
|
+
// context of its own, so a quote inside it does not close the string.
|
|
143
|
+
function closingQuote(text, open, scan, nesting) {
|
|
144
|
+
const quote = text[open];
|
|
145
|
+
let backslashes = 0;
|
|
146
|
+
if (quote === "'" && text[open - 1] === "$") {
|
|
147
|
+
for (let at = open - 2; text[at] === "\\"; at--) {
|
|
148
|
+
scanStep(scan, nesting);
|
|
149
|
+
backslashes++;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const ansi = quote === "'" && text[open - 1] === "$" && backslashes % 2 === 0;
|
|
153
|
+
for (let index = open + 1; index < text.length; index++) {
|
|
154
|
+
scanStep(scan, nesting);
|
|
155
|
+
if (text[index] === quote) return index;
|
|
156
|
+
if (ansi && text[index] === "\\") {
|
|
157
|
+
index++;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (quote === "'") continue;
|
|
161
|
+
const end = substitutionEnd(text, index, scan, nesting + 1);
|
|
162
|
+
if (end === -1) return text.length;
|
|
163
|
+
if (end !== undefined) index = end;
|
|
164
|
+
else if (text[index] === "\\") index++;
|
|
165
|
+
}
|
|
166
|
+
return text.length;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function blankLiteralText(text, scan, nesting) {
|
|
170
|
+
let out = "";
|
|
171
|
+
let index = 0;
|
|
172
|
+
while (index < text.length) {
|
|
173
|
+
scanStep(scan, nesting);
|
|
174
|
+
const end = substitutionEnd(text, index, scan, nesting + 1);
|
|
175
|
+
if (end !== undefined && end !== -1) {
|
|
176
|
+
const open = text[index] === "`" ? 1 : 2;
|
|
177
|
+
out += text.slice(index, index + open) + blankQuotedText(text.slice(index + open, end), scan, nesting + 1) + text[end];
|
|
178
|
+
index = end + 1;
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
const step = text[index] === "\\" ? 2 : 1;
|
|
182
|
+
out += blank(text.slice(index, index + step));
|
|
183
|
+
index += step;
|
|
184
|
+
}
|
|
185
|
+
return out;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// `<<` or `<<-`, optional blanks, then the delimiter word up to an unquoted blank or operator. Quote removal gives the
|
|
189
|
+
// delimiter, and any quoting in the word leaves the body unexpanded.
|
|
190
|
+
function readHeredoc(text, at, scan, nesting) {
|
|
191
|
+
let index = at + 2;
|
|
192
|
+
const stripTabs = text[index] === "-";
|
|
193
|
+
if (stripTabs) index++;
|
|
194
|
+
while (text[index] === " " || text[index] === "\t") {
|
|
195
|
+
scanStep(scan, nesting);
|
|
196
|
+
index++;
|
|
197
|
+
}
|
|
198
|
+
let delimiter = "";
|
|
199
|
+
let quoted = false;
|
|
200
|
+
while (index < text.length && !WORD_END.test(text[index])) {
|
|
201
|
+
scanStep(scan, nesting);
|
|
202
|
+
const char = text[index];
|
|
203
|
+
if (char === "'" || char === '"') {
|
|
204
|
+
const end = text.indexOf(char, index + 1);
|
|
205
|
+
scanStep(scan, nesting, (end === -1 ? text.length : end) - index);
|
|
206
|
+
if (end === -1) return undefined;
|
|
207
|
+
delimiter += text.slice(index + 1, end);
|
|
208
|
+
quoted = true;
|
|
209
|
+
index = end + 1;
|
|
210
|
+
} else if (char === "\\") {
|
|
211
|
+
delimiter += text[index + 1] ?? "";
|
|
212
|
+
quoted = true;
|
|
213
|
+
index += 2;
|
|
214
|
+
} else {
|
|
215
|
+
delimiter += char;
|
|
216
|
+
index++;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return delimiter ? { delimiter, quoted, stripTabs, end: index } : undefined;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function closingLine(text, from, { delimiter, stripTabs }, scan, nesting) {
|
|
223
|
+
let start = from;
|
|
224
|
+
while (start < text.length) {
|
|
225
|
+
const newline = text.indexOf("\n", start);
|
|
226
|
+
const end = newline === -1 ? text.length : newline;
|
|
227
|
+
scanStep(scan, nesting, end - start + 1);
|
|
228
|
+
const line = text.slice(start, end);
|
|
229
|
+
if ((stripTabs ? line.replace(/^\t+/u, "") : line) === delimiter) return { start, end };
|
|
230
|
+
start = end + 1;
|
|
231
|
+
}
|
|
232
|
+
return undefined;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function blankQuotedText(text, scan, nesting) {
|
|
236
|
+
let out = "";
|
|
237
|
+
const heredocs = [];
|
|
238
|
+
let index = 0;
|
|
239
|
+
while (index < text.length) {
|
|
240
|
+
scanStep(scan, nesting);
|
|
241
|
+
const char = text[index];
|
|
242
|
+
if (char === "\\") {
|
|
243
|
+
out += text.slice(index, index + 2);
|
|
244
|
+
index += 2;
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
if (char === "'" || char === '"') {
|
|
248
|
+
const end = closingQuote(text, index, scan, nesting + 1);
|
|
249
|
+
const inner = text.slice(index + 1, end);
|
|
250
|
+
scanStep(scan, nesting, out.length);
|
|
251
|
+
if (SCRIPT_FLAG_BEFORE_QUOTE.test(out)) out += char + blankQuotedText(inner, scan, nesting + 1);
|
|
252
|
+
else out += char + (char === "'" ? blank(inner) : blankLiteralText(inner, scan, nesting + 1));
|
|
253
|
+
if (end < text.length) out += char;
|
|
254
|
+
index = end + 1;
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
if (char === "#" && (index === 0 || BEFORE_COMMENT.test(text[index - 1]))) {
|
|
258
|
+
const newline = text.indexOf("\n", index);
|
|
259
|
+
const end = newline === -1 ? text.length : newline;
|
|
260
|
+
scanStep(scan, nesting, end - index);
|
|
261
|
+
out += blank(text.slice(index, end));
|
|
262
|
+
index = end;
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
if (text.startsWith("<<<", index)) {
|
|
266
|
+
out += "<<<";
|
|
267
|
+
index += 3;
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
const heredoc = text.startsWith("<<", index) ? readHeredoc(text, index, scan, nesting) : undefined;
|
|
271
|
+
if (heredoc) {
|
|
272
|
+
heredocs.push(heredoc);
|
|
273
|
+
out += text.slice(index, heredoc.end);
|
|
274
|
+
index = heredoc.end;
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
out += char;
|
|
278
|
+
index++;
|
|
279
|
+
if (char !== "\n") continue;
|
|
280
|
+
// Each body starts on the line after its `<<` and ends at a line holding only its delimiter.
|
|
281
|
+
for (const pending of heredocs.splice(0)) {
|
|
282
|
+
const close = closingLine(text, index, pending, scan, nesting);
|
|
283
|
+
if (!close) break;
|
|
284
|
+
const body = text.slice(index, close.start);
|
|
285
|
+
out += (pending.quoted ? blank(body) : blankLiteralText(body, scan, nesting + 1)) + text.slice(close.start, close.end);
|
|
286
|
+
index = close.end;
|
|
287
|
+
if (text[index] === "\n") {
|
|
288
|
+
out += "\n";
|
|
289
|
+
index++;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return out;
|
|
294
|
+
}
|
|
295
|
+
|
|
29
296
|
export const GROUP_SIGNAL_NOTICE_TAG = "[senpi:group-signal]";
|
|
30
297
|
|
|
31
298
|
export function signalsProcessGroup(commandText) {
|
|
32
|
-
|
|
299
|
+
// Bound quote masking, but keep a conservative raw-text notice when the scan cannot finish.
|
|
300
|
+
let text = commandText;
|
|
301
|
+
if (commandText.length <= MAX_SCAN_WORK) {
|
|
302
|
+
try {
|
|
303
|
+
text = blankQuotedText(commandText, { remaining: MAX_SCAN_WORK }, 0);
|
|
304
|
+
} catch (error) {
|
|
305
|
+
if (error !== SCAN_LIMIT) throw error;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return KILL_GROUP.test(text) || PKILL_GROUP.test(text) || KILLALL.test(text);
|
|
33
309
|
}
|
|
34
310
|
|
|
35
311
|
export function groupSignalNotice(api, readGroup = agentProcessGroup) {
|