verikun 0.26.2 → 0.26.3
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/.claude/skills/verikun/SKILL.md +2 -1
- package/CHANGELOG.md +10 -0
- package/dist/agent/include.js +140 -13
- package/dist/agent/lint.js +10 -2
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -386,7 +386,8 @@ vk ai onboarding.md --timeout 5m # tighten the run timeout (default 15m)
|
|
|
386
386
|
including file; a fragment may include another. Name a fragment `_something.md` — `vk suite`
|
|
387
387
|
skips `_`-prefixed files, so a fragment never runs as a test of its own. Each chunk compiles
|
|
388
388
|
and caches separately, so editing a shared fragment recompiles the fragment, not every test
|
|
389
|
-
that uses it.
|
|
389
|
+
that uses it. A title or a description states no step, so it is folded into the chunk of its
|
|
390
|
+
own file that does, rather than compiled as a test of its own — put it wherever reads best.
|
|
390
391
|
- The plan expresses **conditions** (`if-present`, for optional interstitials like a
|
|
391
392
|
permission dialog) and **bounded loops** (`repeat … until`, e.g. scroll-until) —
|
|
392
393
|
control flow `vk batch` cannot, so a flaky popup or a scroll-to-find no longer breaks
|
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,16 @@ All notable changes to this project are documented here. The format is based on
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.26.3] - 2026-09-11
|
|
10
|
+
|
|
11
|
+
A `vk ai` test's description is no longer compiled into steps of its own.
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
- **`vk ai`** no longer compiles a title or description above an `@include` as a test of its
|
|
15
|
+
own, inventing steps that then ran first. ([#133])
|
|
16
|
+
|
|
17
|
+
[#133]: https://github.com/ddikman/verikun/issues/133
|
|
18
|
+
|
|
9
19
|
## [0.26.2] - 2026-09-10
|
|
10
20
|
|
|
11
21
|
Android hierarchy reads no longer mistake a sheet's or dialog's barrier for an empty screen.
|
package/dist/agent/include.js
CHANGED
|
@@ -9,25 +9,30 @@
|
|
|
9
9
|
// because the preamble is conditional ("dismiss whichever of these six screens is present"),
|
|
10
10
|
// which is exactly the part only `vk ai` can express.
|
|
11
11
|
//
|
|
12
|
-
//
|
|
12
|
+
// Three properties make the mechanism honest:
|
|
13
13
|
//
|
|
14
14
|
// - **The resolved text is the cache key.** Callers hash `nl` (fragments already inlined),
|
|
15
15
|
// so editing a fragment invalidates every test that includes it. Keying on the top-level
|
|
16
16
|
// file would silently replay stale plans for all of them.
|
|
17
17
|
// - **A fragment is not a test.** `vk suite` skips `_`-prefixed files and never recurses
|
|
18
18
|
// into subdirectories, so a fragment gets no report row and no `--app` data reset.
|
|
19
|
+
// - **A chunk is a whole prompt, so a chunk that is not a test must not become one.** Prose
|
|
20
|
+
// that only describes the test is folded into the chunk of its own file that states the
|
|
21
|
+
// steps it describes, never compiled alone (groupDescriptions, issue #133).
|
|
19
22
|
//
|
|
20
|
-
// `segments` is what makes the compile CHEAP as well as short: each
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
23
|
+
// `segments` is what makes the compile CHEAP as well as short: each chunk of prose is
|
|
24
|
+
// compiled on its own and cached under its own key, so a preamble shared by nine tests is
|
|
25
|
+
// compiled ONCE (see compileFromSegments in cli.ts). Splicing happens at the plan level,
|
|
26
|
+
// which the shallow IR allows — a plan is a flat list of steps.
|
|
24
27
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
28
|
exports.hasInstruction = hasInstruction;
|
|
29
|
+
exports.statesInstruction = statesInstruction;
|
|
26
30
|
exports.resolveIncludes = resolveIncludes;
|
|
27
31
|
exports.segmentLabel = segmentLabel;
|
|
28
32
|
const node_fs_1 = require("node:fs");
|
|
29
33
|
const node_path_1 = require("node:path");
|
|
30
34
|
const errors_1 = require("../errors");
|
|
35
|
+
const lint_1 = require("./lint");
|
|
31
36
|
/** An include directive: `@include <path>` alone on a line (leading space allowed).
|
|
32
37
|
* Deliberately a whole-line form — a path is everything after the keyword, so it needs
|
|
33
38
|
* no quoting and can contain spaces. */
|
|
@@ -47,6 +52,47 @@ function hasInstruction(text) {
|
|
|
47
52
|
const bare = text.replace(/<!--[\s\S]*?-->/g, '');
|
|
48
53
|
return bare.split('\n').some((line) => line.trim() !== '' && !DECORATION_RE.test(line));
|
|
49
54
|
}
|
|
55
|
+
/** A list item of ANY kind — ordered or bulleted. `lint.ts` counts only ORDERED items, because
|
|
56
|
+
* there an unordered bullet carrying explanation would inflate the expected plan size and
|
|
57
|
+
* manufacture a rejection. Here the polarity is reversed: a bulleted preamble
|
|
58
|
+
* ("- Launch the app with its data cleared.") is a list of steps, and missing it is what would
|
|
59
|
+
* do damage. */
|
|
60
|
+
const LIST_ITEM_RE = /^\s*(?:\d+[.)]|[-*+])\s+/;
|
|
61
|
+
/**
|
|
62
|
+
* Does this chunk STATE a step, or only DESCRIBE the test?
|
|
63
|
+
*
|
|
64
|
+
* Deliberately NOT `lint.ts`'s `instructionUnits(text) > 0`, even though that counter answers a
|
|
65
|
+
* neighbouring question. It is documented as biased to UNDERCOUNT, which is safe where it is
|
|
66
|
+
* used ("an undercount only weakens detection, while an overcount would reject a correct plan")
|
|
67
|
+
* and exactly backwards here, where an undercount means calling a real step a description.
|
|
68
|
+
* Measured, all scoring zero units: a bulleted preamble, "First, launch the app.", and this
|
|
69
|
+
* repo's own `_launch-to-home.md` reworded to open with its subject instead of its verb.
|
|
70
|
+
*
|
|
71
|
+
* So this asks for POSITIVE EVIDENCE and errs the other way — a list item of any kind, or one
|
|
72
|
+
* of the same verbs `lint.ts` knows appearing ANYWHERE in a line rather than only at its start.
|
|
73
|
+
* Every misreading it can still make is the safe one: a description that happens to contain
|
|
74
|
+
* "check" or "wait" reads as a step and is compiled alone, which is what happened before this
|
|
75
|
+
* existed. Nothing regresses; some things stop being fabricated.
|
|
76
|
+
*
|
|
77
|
+
* Exported solely so the unit suite can reach it.
|
|
78
|
+
*/
|
|
79
|
+
function statesInstruction(text) {
|
|
80
|
+
let fenced = false;
|
|
81
|
+
for (const line of text.replace(/<!--[\s\S]*?-->/g, '').split('\n')) {
|
|
82
|
+
if (FENCE_RE.test(line)) {
|
|
83
|
+
fenced = !fenced;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (fenced)
|
|
87
|
+
continue;
|
|
88
|
+
if (LIST_ITEM_RE.test(line))
|
|
89
|
+
return true;
|
|
90
|
+
for (const word of line.toLowerCase().match(/[a-z]+/g) ?? [])
|
|
91
|
+
if (lint_1.IMPERATIVES.has(word))
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
50
96
|
const endWithNewline = (s) => (s.endsWith('\n') ? s : `${s}\n`);
|
|
51
97
|
const readFile = (path) => (0, node_fs_1.readFileSync)(path, 'utf8');
|
|
52
98
|
/**
|
|
@@ -58,17 +104,21 @@ const readFile = (path) => (0, node_fs_1.readFileSync)(path, 'utf8');
|
|
|
58
104
|
* otherwise expand until it ran out of memory.
|
|
59
105
|
*/
|
|
60
106
|
function resolveIncludes(file, read = readFile) {
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
107
|
+
const ex = { out: [], read, units: 0 };
|
|
108
|
+
// `nl` is assembled from `parts`, independently of `ex.out`, so regrouping the chunks below
|
|
109
|
+
// cannot change the resolved text — and therefore cannot change the whole-test cache key, nor
|
|
110
|
+
// what the assembled-plan lint is asked about.
|
|
111
|
+
const nl = expand((0, node_path_1.resolve)(process.cwd(), file), [], ex, null);
|
|
112
|
+
return { nl, segments: groupDescriptions(ex.out) };
|
|
64
113
|
}
|
|
65
|
-
function expand(path, stack,
|
|
114
|
+
function expand(path, stack, ex, from) {
|
|
115
|
+
const unit = ex.units++;
|
|
66
116
|
if (stack.includes(path)) {
|
|
67
117
|
throw new errors_1.CliError(`ai: include cycle — ${[...stack, path].map((p) => shortName(p)).join(' → ')}`, 2);
|
|
68
118
|
}
|
|
69
119
|
let text;
|
|
70
120
|
try {
|
|
71
|
-
text = read(path);
|
|
121
|
+
text = ex.read(path);
|
|
72
122
|
}
|
|
73
123
|
catch (e) {
|
|
74
124
|
const where = from ? ` (included from ${from})` : '';
|
|
@@ -91,8 +141,9 @@ function expand(path, stack, out, read, from) {
|
|
|
91
141
|
// The label points at the chunk's first real line, not at the blank line that
|
|
92
142
|
// separated it from the include above it.
|
|
93
143
|
const lead = buf.findIndex((l) => l.trim() !== '');
|
|
94
|
-
if (lead >= 0)
|
|
95
|
-
out.push({ text: chunk, source: path, startLine: bufStart + lead, compilable: hasInstruction(chunk) });
|
|
144
|
+
if (lead >= 0) {
|
|
145
|
+
ex.out.push({ text: chunk, source: path, startLine: bufStart + lead, compilable: hasInstruction(chunk), unit });
|
|
146
|
+
}
|
|
96
147
|
buf = [];
|
|
97
148
|
};
|
|
98
149
|
lines.forEach((line, i) => {
|
|
@@ -106,11 +157,87 @@ function expand(path, stack, out, read, from) {
|
|
|
106
157
|
return;
|
|
107
158
|
}
|
|
108
159
|
flush();
|
|
109
|
-
parts.push(endWithNewline(expand((0, node_path_1.resolve)((0, node_path_1.dirname)(path), m[1]), [...stack, path],
|
|
160
|
+
parts.push(endWithNewline(expand((0, node_path_1.resolve)((0, node_path_1.dirname)(path), m[1]), [...stack, path], ex, `${shortName(path)}:${i + 1}`)));
|
|
110
161
|
});
|
|
111
162
|
flush();
|
|
112
163
|
return parts.join('');
|
|
113
164
|
}
|
|
165
|
+
/**
|
|
166
|
+
* The chunk a description folds into: the next chunk of its OWN expansion that states a step,
|
|
167
|
+
* else the previous one. `-1` when that expansion states no step anywhere.
|
|
168
|
+
*
|
|
169
|
+
* Scanning by EXPANSION rather than by path is what keeps a fragment included twice identical in
|
|
170
|
+
* both places. Scanning by FILE rather than simply "the next chunk" is the load-bearing half:
|
|
171
|
+
* the next chunk is normally the fragment the `@include` pulled in, and folding a per-test
|
|
172
|
+
* description into it would change the FRAGMENT's cache key per test, destroying the
|
|
173
|
+
* compiled-once property `@include` exists for. Another expansion's chunks are skipped OVER, not
|
|
174
|
+
* stopped at — reaching across the include to the file's own next chunk is the entire point.
|
|
175
|
+
*/
|
|
176
|
+
function hostFor(raw, describes, i) {
|
|
177
|
+
const usable = (j) => raw[j].unit === raw[i].unit && raw[j].compilable && !describes[j];
|
|
178
|
+
for (let j = i + 1; j < raw.length; j++)
|
|
179
|
+
if (usable(j))
|
|
180
|
+
return j;
|
|
181
|
+
for (let j = i - 1; j >= 0; j--)
|
|
182
|
+
if (usable(j))
|
|
183
|
+
return j;
|
|
184
|
+
return -1;
|
|
185
|
+
}
|
|
186
|
+
const plain = ({ text, source, startLine, compilable }) => ({ text, source, startLine, compilable });
|
|
187
|
+
/**
|
|
188
|
+
* Fold a chunk that only DESCRIBES the test into the chunk of the same file that states the
|
|
189
|
+
* steps it describes (issue #133).
|
|
190
|
+
*
|
|
191
|
+
* A title and a summary paragraph written above the first `@include` used to become a chunk of
|
|
192
|
+
* its own — and a chunk is a whole prompt. Compiled alone, the summary IS the test, so the model
|
|
193
|
+
* invents a plan for it and those steps are spliced AHEAD of the launch the include was there to
|
|
194
|
+
* perform; the reported case was 34 fabricated steps whose first act was to tap a login button on
|
|
195
|
+
* the Android launcher. `SECTION_NOTE` has told the model since `@include` shipped that a summary
|
|
196
|
+
* is not an instruction, and it still went the wrong way on 5 of 14 tests in one run: whether
|
|
197
|
+
* prose reads as a spec is a model judgement, so the fix has to be deterministic.
|
|
198
|
+
*
|
|
199
|
+
* A STRICT IMPROVEMENT, deliberately: every chunk either folds into a neighbour or is left
|
|
200
|
+
* exactly as it was. There is no third disposition, and in particular nothing is DROPPED — prose
|
|
201
|
+
* this pass discarded would be prose the model never sees, and a fragment wrongly discarded is a
|
|
202
|
+
* suite that silently stops launching its app. No coverage rule could catch that: the floor and
|
|
203
|
+
* the tail anchors both measure `nl`, which this pass does not touch.
|
|
204
|
+
*/
|
|
205
|
+
function groupDescriptions(raw) {
|
|
206
|
+
const describes = raw.map((s) => s.compilable && !statesInstruction(s.text));
|
|
207
|
+
const host = raw.map((_, i) => (describes[i] ? hostFor(raw, describes, i) : -1));
|
|
208
|
+
// The overwhelmingly common case — no description, or one with nowhere to go — allocates
|
|
209
|
+
// nothing and is byte-identical to the behaviour before this pass existed.
|
|
210
|
+
if (host.every((h) => h < 0))
|
|
211
|
+
return raw.map(plain);
|
|
212
|
+
const out = [];
|
|
213
|
+
const slot = new Map(); // index in `raw` -> index in `out`
|
|
214
|
+
raw.forEach((s, i) => {
|
|
215
|
+
if (host[i] >= 0)
|
|
216
|
+
return; // folded into its host below
|
|
217
|
+
slot.set(i, out.length);
|
|
218
|
+
out.push(plain(s));
|
|
219
|
+
});
|
|
220
|
+
// Prepends run BACKWARDS and appends run FORWARDS. Not a style choice: each fold lands at the
|
|
221
|
+
// host's edge, so whichever is written LAST ends up nearest the host. A file with two
|
|
222
|
+
// descriptions separated by two includes has to come out in the order its author wrote them,
|
|
223
|
+
// and a single pass in either direction reverses one of those two chains.
|
|
224
|
+
for (let i = raw.length - 1; i >= 0; i--) {
|
|
225
|
+
if (host[i] <= i)
|
|
226
|
+
continue;
|
|
227
|
+
const at = slot.get(host[i]);
|
|
228
|
+
// The earlier line, so `segmentLabel` names where the folded text now begins. That label is
|
|
229
|
+
// the accounting for the fold: a chunk that printed as `t.md:8` prints as `t.md:1`, and the
|
|
230
|
+
// "assembled from N chunk(s)" line drops by one. No prose moves unannounced.
|
|
231
|
+
out[at] = { ...out[at], text: raw[i].text + out[at].text, startLine: Math.min(out[at].startLine, raw[i].startLine) };
|
|
232
|
+
}
|
|
233
|
+
for (let i = 0; i < raw.length; i++) {
|
|
234
|
+
if (host[i] < 0 || host[i] > i)
|
|
235
|
+
continue;
|
|
236
|
+
const at = slot.get(host[i]);
|
|
237
|
+
out[at] = { ...out[at], text: out[at].text + raw[i].text };
|
|
238
|
+
}
|
|
239
|
+
return out;
|
|
240
|
+
}
|
|
114
241
|
/** A path as a human reads it: relative to the cwd when it is below it. */
|
|
115
242
|
function shortName(path) {
|
|
116
243
|
const cwd = `${process.cwd()}/`;
|
package/dist/agent/lint.js
CHANGED
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
// rule requires a fairly unambiguous phrase and checks for a specific structural counterpart.
|
|
29
29
|
// It never edits the plan; the model gets the feedback and stays the author.
|
|
30
30
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
31
|
+
exports.IMPERATIVES = void 0;
|
|
31
32
|
exports.coverageChecksEnabled = coverageChecksEnabled;
|
|
32
33
|
exports.instructionLines = instructionLines;
|
|
33
34
|
exports.instructionUnits = instructionUnits;
|
|
@@ -87,8 +88,15 @@ const FENCE_RE = /^\s*(```|~~~)/;
|
|
|
87
88
|
* Verbs that open an instruction. A CLOSED set, and a tight one — every entry here raises
|
|
88
89
|
* the expected plan size, so a loose entry costs a false rejection while a missing one only
|
|
89
90
|
* costs detection. The floor below is generous enough to absorb the resulting undercount.
|
|
91
|
+
*
|
|
92
|
+
* Exported for ONE other consumer, `agent/include.ts`, which asks a different question of the
|
|
93
|
+
* same vocabulary: not "how many steps does this prose state" but "does it state one at all".
|
|
94
|
+
* Shared rather than copied for the reason `pidAlive` is shared with `agent/plan-lock.ts` — a
|
|
95
|
+
* second list of verbs drifts from this one invisibly. Note that it is NOT part of
|
|
96
|
+
* `COMPILER_FINGERPRINT`, so an edit here moves a chunk boundary as well as a lint threshold;
|
|
97
|
+
* the version bump such an edit already requires is what rotates the fingerprint.
|
|
90
98
|
*/
|
|
91
|
-
|
|
99
|
+
exports.IMPERATIVES = new Set([
|
|
92
100
|
'tap', 'press', 'click', 'type', 'enter', 'fill',
|
|
93
101
|
'launch', 'open', 'start', 'restart', 'relaunch', 'stop', 'close',
|
|
94
102
|
'confirm', 'verify', 'assert', 'check', 'ensure',
|
|
@@ -121,7 +129,7 @@ function instructionLines(nl) {
|
|
|
121
129
|
const body = (ordered ? raw.replace(ORDERED_ITEM_RE, '') : raw).trim();
|
|
122
130
|
if (!body)
|
|
123
131
|
continue;
|
|
124
|
-
if (ordered || IMPERATIVES.has((/^([A-Za-z]+)/.exec(body)?.[1] ?? '').toLowerCase()))
|
|
132
|
+
if (ordered || exports.IMPERATIVES.has((/^([A-Za-z]+)/.exec(body)?.[1] ?? '').toLowerCase()))
|
|
125
133
|
out.push(body);
|
|
126
134
|
}
|
|
127
135
|
return out;
|
package/dist/version.js
CHANGED
|
@@ -3,4 +3,4 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.VERSION = void 0;
|
|
4
4
|
// GENERATED by scripts/gen-version.mjs from package.json's "version" at build time
|
|
5
5
|
// (the `prebuild` script). Do NOT edit by hand; bump package.json instead.
|
|
6
|
-
exports.VERSION = '0.26.
|
|
6
|
+
exports.VERSION = '0.26.3';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "verikun",
|
|
3
|
-
"version": "0.26.
|
|
3
|
+
"version": "0.26.3",
|
|
4
4
|
"description": "Drive Android emulators/devices and iOS simulators for AI agents: tap, type, swipe, screenshot, and inspect the UI hierarchy by semantic identifiers — like Puppeteer for native apps.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"android",
|