scenescout 3.5.0 โ 3.6.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.md +6 -0
- package/README.md +1 -1
- package/dist/engine/memory.js +4 -1
- package/dist/engine/pace.js +55 -21
- package/dist/mcp-server.js +3 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# scenescout
|
|
2
2
|
|
|
3
|
+
## 3.6.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 6fbd659: The pace section now separates a lane's working time from the time it holds its browser after finishing. "Idle while working" is the share of each lane's working time spent in gaps over 30 seconds; "after finishing" is time waiting to be collected and closed, split at the moment the lane's report was folded. The latter grows with the number of lanes and the slowest one, so it is reported as planner overhead rather than summed into the lanes' idle time. `scout_lane_report` now logs the fold so the split can be made.
|
|
8
|
+
|
|
3
9
|
## 3.5.0
|
|
4
10
|
|
|
5
11
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -335,7 +335,7 @@ A `๐ก WRITE-POLICY blocked` notice is the safety net doing its job, not an app
|
|
|
335
335
|
- ๐ฏ **Page scores** (0โ100: a11y ยท craft ยท consistency ยท task-clarity), ranked worst-first, with stale scores from old runs marked as such.
|
|
336
336
|
- ๐ฅ **A role capability matrix** โ what each role could and couldn't reach.
|
|
337
337
|
- ๐งพ **A gap ledger** โ everything *not* done, so the report is honest about its own coverage.
|
|
338
|
-
- โฑ๏ธ **How the run was paced** โ
|
|
338
|
+
- โฑ๏ธ **How the run was paced** โ how closely each session kept working, and apart from that, how long finished lanes held their browsers waiting to be collected, so neither hides the other.
|
|
339
339
|
- ๐ฏ **How well the lanes judged** โ on a parallel run, whether the confidence each lane stated matched what the project went on to file, beside what later re-tests found ([ADR 10](docs/adr/0010-a-confidence-is-checked-not-trusted.md)).
|
|
340
340
|
|
|
341
341
|
`.scenescout/report.html` โ the same report as one self-contained page, with every session's trail beside it, and on a [recorded run](#-recording-a-run-and-reading-it-back) the screenshots under each finding.
|
package/dist/engine/memory.js
CHANGED
|
@@ -1212,7 +1212,10 @@ export class MemoryStore {
|
|
|
1212
1212
|
return url;
|
|
1213
1213
|
}
|
|
1214
1214
|
};
|
|
1215
|
-
|
|
1215
|
+
// The planner's fold marker is not a step anyone took on a page. Left in,
|
|
1216
|
+
// its empty URL read as a route change and cut the trace of a finding filed
|
|
1217
|
+
// right after a fold โ which is when the fold asks lanes to file.
|
|
1218
|
+
const log = this.actionLog.filter((a) => a.action !== "lane-report");
|
|
1216
1219
|
let start = Math.max(0, log.length - 12);
|
|
1217
1220
|
for (let i = log.length - 1; i >= 0 && i >= log.length - 12; i--) {
|
|
1218
1221
|
if (routeOf(log[i].url) !== routeOf(f.url)) {
|
package/dist/engine/pace.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* counting them inflates the action count and drags the median gap toward
|
|
5
5
|
* zero. One real run logged 325 entries of which 107 were stated tasks.
|
|
6
6
|
*/
|
|
7
|
-
const MARKER_ACTIONS = new Set(["task", "attach", "close", "created-resource", "journey:start", "journey:end", "record:full", "record:failed"]);
|
|
7
|
+
const MARKER_ACTIONS = new Set(["task", "attach", "close", "lane-report", "created-resource", "journey:start", "journey:end", "record:full", "record:failed"]);
|
|
8
8
|
/** Whether a log entry represents work the browser actually did. */
|
|
9
9
|
export function isActing(action) {
|
|
10
10
|
return !MARKER_ACTIONS.has(action);
|
|
@@ -30,7 +30,11 @@ export function measurePace(log, nowMs, attached = []) {
|
|
|
30
30
|
const lifecycle = new Map();
|
|
31
31
|
for (const entry of log) {
|
|
32
32
|
const name = entry.session ?? "default";
|
|
33
|
-
const into = isActing(entry.action)
|
|
33
|
+
const into = isActing(entry.action)
|
|
34
|
+
? bySession
|
|
35
|
+
: entry.action === "attach" || entry.action === "close" || entry.action === "lane-report"
|
|
36
|
+
? lifecycle
|
|
37
|
+
: null;
|
|
34
38
|
if (!into)
|
|
35
39
|
continue;
|
|
36
40
|
const list = into.get(name);
|
|
@@ -51,10 +55,10 @@ export function measurePace(log, nowMs, attached = []) {
|
|
|
51
55
|
.map((e) => Date.parse(e.at))
|
|
52
56
|
.filter((t) => Number.isFinite(t))
|
|
53
57
|
.sort((a, b) => a - b);
|
|
54
|
-
const
|
|
58
|
+
const edge = edges(lifecycle.get(session) ?? [], times, nowMs, stillOpen.has(session));
|
|
55
59
|
if (times.length === 0) {
|
|
56
|
-
if (
|
|
57
|
-
sessions.push({ session, actions: 0, spanMs: 0, medianGapMs: 0, maxGapMs: 0, idleShare: 0, quietMs: 0, framed: 0,
|
|
60
|
+
if (edge.leadInMs > 0) {
|
|
61
|
+
sessions.push({ session, actions: 0, spanMs: 0, medianGapMs: 0, maxGapMs: 0, idleShare: 0, quietMs: 0, framed: 0, ...edge });
|
|
58
62
|
}
|
|
59
63
|
continue;
|
|
60
64
|
}
|
|
@@ -75,11 +79,20 @@ export function measurePace(log, nowMs, attached = []) {
|
|
|
75
79
|
idleShare: spanMs > 0 ? idleMs / spanMs : 0,
|
|
76
80
|
quietMs: Math.max(0, nowMs - times[times.length - 1]),
|
|
77
81
|
framed: entries.filter((e) => e.frame).length,
|
|
78
|
-
|
|
82
|
+
...edge,
|
|
79
83
|
});
|
|
80
84
|
}
|
|
81
85
|
sessions.sort((a, b) => b.actions - a.actions);
|
|
86
|
+
const folds = sessions.filter((s) => s.afterFoldMs !== null);
|
|
87
|
+
const waiting = {
|
|
88
|
+
workingMs: sessions.reduce((sum, s) => sum + s.spanMs, 0),
|
|
89
|
+
workingIdleMs: sessions.reduce((sum, s) => sum + s.idleShare * s.spanMs, 0),
|
|
90
|
+
leadInMs: sessions.reduce((sum, s) => sum + s.leadInMs, 0),
|
|
91
|
+
afterFinishMs: sessions.reduce((sum, s) => sum + s.afterFinishMs, 0),
|
|
92
|
+
afterFoldMs: folds.length > 0 ? folds.reduce((sum, s) => sum + (s.afterFoldMs ?? 0), 0) : null,
|
|
93
|
+
};
|
|
82
94
|
return {
|
|
95
|
+
waiting,
|
|
83
96
|
sessions,
|
|
84
97
|
spanMs: sessions.length > 0 ? last - first : 0,
|
|
85
98
|
actions: sessions.reduce((sum, x) => sum + x.actions, 0),
|
|
@@ -91,18 +104,22 @@ export function measurePace(log, nowMs, attached = []) {
|
|
|
91
104
|
};
|
|
92
105
|
}
|
|
93
106
|
/**
|
|
94
|
-
*
|
|
95
|
-
* to close. An attach with no close after it ends at
|
|
96
|
-
* now while the session is still open. A log written
|
|
97
|
-
* recorded has no close marker; its closed sessions count no
|
|
98
|
-
* because when they closed is unknown.
|
|
107
|
+
* Time at the edges of each attach: attach to first action (lead-in), and last
|
|
108
|
+
* action to close (after finishing). An attach with no close after it ends at
|
|
109
|
+
* the next attach, or at now while the session is still open. A log written
|
|
110
|
+
* before close was recorded has no close marker; its closed sessions count no
|
|
111
|
+
* trailing time, because when they closed is unknown. A fold marker after the
|
|
112
|
+
* last action splits the trailing time at the moment the browser stopped being
|
|
113
|
+
* needed.
|
|
99
114
|
*/
|
|
100
|
-
function
|
|
115
|
+
function edges(markers, actionTimes, nowMs, stillOpen) {
|
|
101
116
|
const events = markers
|
|
102
117
|
.map((m) => ({ kind: m.action, at: Date.parse(m.at) }))
|
|
103
118
|
.filter((e) => Number.isFinite(e.at))
|
|
104
119
|
.sort((a, b) => a.at - b.at);
|
|
105
|
-
let
|
|
120
|
+
let leadInMs = 0;
|
|
121
|
+
let afterFinishMs = 0;
|
|
122
|
+
let afterFoldMs = null;
|
|
106
123
|
for (let i = 0; i < events.length; i += 1) {
|
|
107
124
|
if (events[i].kind !== "attach")
|
|
108
125
|
continue;
|
|
@@ -112,14 +129,19 @@ function heldIdle(markers, actionTimes, nowMs, stillOpen) {
|
|
|
112
129
|
const inside = actionTimes.filter((t) => t >= start && (end === null || t <= end));
|
|
113
130
|
if (inside.length === 0) {
|
|
114
131
|
if (end !== null)
|
|
115
|
-
|
|
132
|
+
leadInMs += end - start;
|
|
116
133
|
continue;
|
|
117
134
|
}
|
|
118
|
-
|
|
119
|
-
if (end
|
|
120
|
-
|
|
135
|
+
leadInMs += inside[0] - start;
|
|
136
|
+
if (end === null)
|
|
137
|
+
continue;
|
|
138
|
+
const lastAction = inside[inside.length - 1];
|
|
139
|
+
afterFinishMs += end - lastAction;
|
|
140
|
+
const fold = events.filter((e) => e.kind === "lane-report" && e.at >= lastAction && e.at <= end).at(-1);
|
|
141
|
+
if (fold)
|
|
142
|
+
afterFoldMs = (afterFoldMs ?? 0) + (end - fold.at);
|
|
121
143
|
}
|
|
122
|
-
return Math.max(0,
|
|
144
|
+
return { leadInMs: Math.max(0, leadInMs), afterFinishMs: Math.max(0, afterFinishMs), afterFoldMs };
|
|
123
145
|
}
|
|
124
146
|
/** Milliseconds as a person says them: 45s, 4m12s, 1h03m. */
|
|
125
147
|
export function sayDuration(ms) {
|
|
@@ -139,16 +161,28 @@ export function sayDuration(ms) {
|
|
|
139
161
|
export function formatPace(pace) {
|
|
140
162
|
if (pace.sessions.length === 0)
|
|
141
163
|
return [];
|
|
164
|
+
const w = pace.waiting;
|
|
165
|
+
const pct = (part, whole) => (whole > 0 ? `${Math.round((part / whole) * 100)}%` : "0%");
|
|
142
166
|
const lines = [
|
|
143
167
|
`## How the run was paced`,
|
|
144
168
|
``,
|
|
145
|
-
`${pace.actions} action(s) over ${sayDuration(pace.spanMs)}. Stated tasks and attaches are not counted: they take no time
|
|
169
|
+
`${pace.actions} action(s) over ${sayDuration(pace.spanMs)}. Stated tasks and attaches are not counted: they take no time.`,
|
|
170
|
+
``,
|
|
171
|
+
`**While working** โ first action to last, ${sayDuration(w.workingMs)} across sessions โ ${pct(w.workingIdleMs, w.workingMs)} was spent in gaps over ${sayDuration(IDLE_GAP_MS)}, the agent thinking at length. That is how closely the sessions kept working.`,
|
|
172
|
+
``,
|
|
173
|
+
`**After finishing**, sessions held their browsers for a further ${sayDuration(w.afterFinishMs)}` +
|
|
174
|
+
(w.afterFoldMs !== null ? `, ${sayDuration(w.afterFoldMs)} of it after their report was already folded` : ``) +
|
|
175
|
+
(w.afterFoldMs !== null
|
|
176
|
+
? `. That is time waiting to be collected and closed: it grows with the number of lanes and with the slowest one, not with how well any lane worked. Closing each lane as soon as its report is folded removes it.`
|
|
177
|
+
: `: time from the last action to close.`) +
|
|
178
|
+
` (Before the first action: ${sayDuration(w.leadInMs)}.)`,
|
|
146
179
|
``,
|
|
147
|
-
`| Session | Actions |
|
|
180
|
+
`| Session | Actions | Working | Median gap | Longest gap | Idle while working | After finishing | Frames |`,
|
|
148
181
|
`|---|---:|---:|---:|---:|---:|---:|---:|`,
|
|
149
182
|
];
|
|
150
183
|
for (const s of pace.sessions) {
|
|
151
|
-
|
|
184
|
+
const after = sayDuration(s.afterFinishMs) + (s.afterFoldMs !== null ? ` (${sayDuration(s.afterFoldMs)} after fold)` : ``);
|
|
185
|
+
lines.push(`| ${s.session} | ${s.actions} | ${sayDuration(s.spanMs)} | ${sayDuration(s.medianGapMs)} | ${sayDuration(s.maxGapMs)} | ${Math.round(s.idleShare * 100)}% | ${after} | ${s.framed} |`);
|
|
152
186
|
}
|
|
153
187
|
lines.push(``);
|
|
154
188
|
if (pace.quiet.length > 0) {
|
package/dist/mcp-server.js
CHANGED
|
@@ -546,6 +546,9 @@ server.registerTool("scout_lane_report", {
|
|
|
546
546
|
// already closed makes that the ordinary case, not an edge one.
|
|
547
547
|
const owner = engines.get(lane)?.memory;
|
|
548
548
|
const at = new Date().toISOString();
|
|
549
|
+
// Marks the moment the lane's browser stopped being needed, so the pace
|
|
550
|
+
// section can tell a lane still reporting from one waiting to be closed.
|
|
551
|
+
owner?.logAction({ action: "lane-report", url: engines.get(lane)?.currentUrl ?? "", session: lane });
|
|
549
552
|
const kept = owner
|
|
550
553
|
? owner.addLaneDecisions(lane, parsed.report.decisions.map((d) => ({ ...d, lane, at })))
|
|
551
554
|
: 0;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "scenescout",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.6.0",
|
|
4
4
|
"description": "SceneScout โ exploratory UI testing for AI coding agents. An MCP server that gives any agent (Claude Code, Cursor, VS Code Copilot, Codex, Gemini CLI and others) a structured view of a running web app, always-on oracles, a network-level write policy, memory across runs and a gap-checked report.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "brunoboto96",
|