zerocheck 0.1.2 → 0.1.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.
Files changed (3) hide show
  1. package/README.md +3 -3
  2. package/dist/index.js +61 -16
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -9,7 +9,7 @@ Paste a checklist, review one drafted check and browser verification result per
9
9
  Requires Node.js 20.19 or newer and a Zerocheck project/account. Create a project in the web app’s Settings, then authorize a device or create a project token.
10
10
 
11
11
  ```sh
12
- npm install --save-dev zerocheck@0.1.2
12
+ npm install --save-dev zerocheck@0.1.3
13
13
  npx zerocheck login
14
14
  npx zerocheck init --project your-project-id --url http://localhost:3000
15
15
  npx zerocheck install
@@ -149,7 +149,7 @@ Use the installed CLI as a stdio MCP server, with an explicit project directory:
149
149
  "mcpServers": {
150
150
  "zerocheck": {
151
151
  "command": "npx",
152
- "args": ["--yes", "zerocheck@0.1.2", "mcp", "--project-dir", "/absolute/path/to/project"]
152
+ "args": ["--yes", "zerocheck@0.1.3", "mcp", "--project-dir", "/absolute/path/to/project"]
153
153
  }
154
154
  }
155
155
  }
@@ -167,6 +167,6 @@ Local execution sends selected check YAML, the page context needed for AI (inclu
167
167
 
168
168
  ## Repository development
169
169
 
170
- The CLI bundles the shared engine source into `dist/index.js`; it has no unpublished engine-package dependency. From this repository, run `npm run build`, `npm run check`, and `npm test` in `packages/cli`. Tests include a real MCP client communicating with the bundled CLI over stdio. The generated workflow and documentation use version `0.1.2`; update that pin together with the package version for a release.
170
+ The CLI bundles the shared engine source into `dist/index.js`; it has no unpublished engine-package dependency. From this repository, run `npm run build`, `npm run check`, and `npm test` in `packages/cli`. Tests include a real MCP client communicating with the bundled CLI over stdio. The generated workflow and documentation use version `0.1.3`; update that pin together with the package version for a release.
171
171
 
172
172
  Hosted runs and imports return a queued handle when browsers are occupied; CLI and MCP keep polling until they finish. The queue holds 20 waiting jobs for up to five minutes and never automatically resumes interrupted jobs after restart. Recent imports is available in the web app’s Checks page.
package/dist/index.js CHANGED
@@ -6979,7 +6979,7 @@ async function loginCommand() {
6979
6979
  let device;
6980
6980
  try {
6981
6981
  device = await apiRequest("/v1/cli/auth/device", {
6982
- body: { device_name: deviceName, client_version: "0.1.2" },
6982
+ body: { device_name: deviceName, client_version: "0.1.3" },
6983
6983
  authed: false
6984
6984
  });
6985
6985
  } catch (err) {
@@ -7492,16 +7492,47 @@ async function isFrameVisible(frame) {
7492
7492
  return true;
7493
7493
  }
7494
7494
 
7495
+ // ../../src/agent/observation-recovery.ts
7496
+ function isTransientObservationError(error2) {
7497
+ const message = error2 instanceof Error ? error2.message : String(error2);
7498
+ if (/closed|cancel|abort|policy|ambiguous/i.test(message)) return false;
7499
+ return /Execution context was destroyed|Cannot find context with specified id|Frame was detached|frame has been detached|Stale computer-use observation: frame/i.test(message);
7500
+ }
7501
+ async function retryObservation(page, read, options = {}) {
7502
+ const deadline = Date.now() + (options.timeoutMs ?? 5e3);
7503
+ for (let attempt = 0; ; attempt++) {
7504
+ options.signal?.throwIfAborted();
7505
+ if (page.isClosed()) throw new Error("Browser page closed during observation.");
7506
+ const remaining = deadline - Date.now();
7507
+ if (remaining <= 0) throw new Error("Browser observation timed out before a complete snapshot was available.");
7508
+ try {
7509
+ const value = await read(remaining);
7510
+ options.signal?.throwIfAborted();
7511
+ if (Date.now() > deadline) throw new Error("Browser observation timed out before a complete snapshot was available.");
7512
+ return value;
7513
+ } catch (error2) {
7514
+ if (attempt >= 2 || page.isClosed() || options.signal?.aborted || !isTransientObservationError(error2) || deadline - Date.now() <= 50) throw error2;
7515
+ options.onRetry?.(error2);
7516
+ await page.waitForTimeout(50);
7517
+ }
7518
+ }
7519
+ }
7520
+
7495
7521
  // ../../src/agent/page-state.ts
7496
- async function capturePageState(page, logger, secrets = [], targetPolicy) {
7522
+ async function capturePageState(page, logger, secrets = [], targetPolicy, options = {}) {
7523
+ return retryObservation(page, (remaining) => capturePageStateOnce(page, logger, secrets, targetPolicy, remaining), options);
7524
+ }
7525
+ async function capturePageStateOnce(page, logger, secrets, targetPolicy, timeoutMs) {
7497
7526
  targetPolicy?.assertCurrentUrl(page.url());
7498
7527
  const policy = observationPolicy(page, targetPolicy);
7499
- const [screenshot, snapshot, url2, title] = await Promise.all([
7500
- captureScreenshot(page, 5e3, secrets, policy),
7528
+ const observations = await Promise.allSettled([
7529
+ captureScreenshotOnce(page, timeoutMs, secrets, policy),
7501
7530
  getAccessibilitySnapshot(page, logger, secrets),
7502
7531
  page.url(),
7503
7532
  page.title()
7504
7533
  ]);
7534
+ for (const observation of observations) if (observation.status === "rejected") throw observation.reason;
7535
+ const [screenshot, snapshot, url2, title] = observations.map((observation) => observation.value);
7505
7536
  const frameTrees = [];
7506
7537
  const blocked = /* @__PURE__ */ new Set();
7507
7538
  for (const frame of page.frames()) {
@@ -7604,6 +7635,9 @@ function buildTreeFromCDP(nodes) {
7604
7635
  return nodeMap.get(nodes[0].nodeId) ?? null;
7605
7636
  }
7606
7637
  async function captureScreenshot(page, timeoutMs = 5e3, secrets = [], targetPolicy) {
7638
+ return retryObservation(page, (remaining) => captureScreenshotOnce(page, remaining, secrets, targetPolicy), { timeoutMs });
7639
+ }
7640
+ async function captureScreenshotOnce(page, timeoutMs, secrets, targetPolicy) {
7607
7641
  const policy = observationPolicy(page, targetPolicy);
7608
7642
  const mask = [];
7609
7643
  const owners = [];
@@ -8125,8 +8159,8 @@ async function waitForVisibleText(page, assertion, timeoutMs, signal, targetPoli
8125
8159
  targetPolicy?.assertCurrentUrl(page.url());
8126
8160
  let found = false;
8127
8161
  for (const frame of targetPolicy ? page.frames() : [page.mainFrame()]) {
8128
- if (targetPolicy && (await blockedFrameOrigins(frame, targetPolicy)).length) continue;
8129
8162
  if (!await isFrameVisible(frame)) continue;
8163
+ if (targetPolicy && (await blockedFrameOrigins(frame, targetPolicy)).length) continue;
8130
8164
  const locator = frame.getByText(assertion.text, { exact: true });
8131
8165
  const count = await locator.count();
8132
8166
  for (let index = 0; index < count; index++) {
@@ -9746,8 +9780,12 @@ var BrowserAgent = class {
9746
9780
  const before = /* @__PURE__ */ new Map();
9747
9781
  for (const assertion of this.config.assertions ?? []) {
9748
9782
  if (step.lineNumber >= 0 ? assertion.lineNumber <= step.lineNumber : assertion.lineNumber < 0 && assertion.lineNumber >= step.lineNumber) continue;
9749
- const observed = await deterministicPredicate(page, assertion.argument, this.config.targetPolicy);
9750
- if (observed !== void 0) before.set(assertion.raw, observed);
9783
+ try {
9784
+ const observed = await deterministicPredicate(page, assertion.argument, this.config.targetPolicy);
9785
+ if (observed !== void 0) before.set(assertion.raw, observed);
9786
+ } catch (error2) {
9787
+ if (!isTransientObservationError(error2) || page.isClosed() || this.signal?.aborted) throw error2;
9788
+ }
9751
9789
  }
9752
9790
  const result = await this.withAI(page, step, start, STEP_EXECUTION_SYSTEM, lookup.reason, establishedIdentity);
9753
9791
  this.signal?.throwIfAborted();
@@ -9788,7 +9826,7 @@ var BrowserAgent = class {
9788
9826
  for (let attempt = 0; attempt < 2; attempt++) {
9789
9827
  let executionStarted = false;
9790
9828
  try {
9791
- const state = await capturePageState(page, this.config.logger, this.config.secrets, this.config.targetPolicy);
9829
+ const state = await this.observe(page);
9792
9830
  const plan = await this.plan(step, state, system, recovery);
9793
9831
  this.signal?.throwIfAborted();
9794
9832
  const groundedTarget = await resolveVisualTarget(page, plan, state, this.config.secrets ?? [], this.config.targetPolicy);
@@ -9798,13 +9836,20 @@ var BrowserAgent = class {
9798
9836
  return pass(step, start, { resolvedVia: "ai", confidence: plan.confidence, _resolvedPlan: plan });
9799
9837
  } catch (error2) {
9800
9838
  const planningRetry = !executionStarted && /temporar|unavailable|rate limit|429|50[234]|ECONNRESET|fetch failed/i.test(String(error2));
9801
- if (attempt || this.signal?.aborted || planningRetry && this.config.retries === 0 || !planningRetry && !safeToReground(error2, this.signal) && !/Stale computer-use observation/.test(String(error2))) throw error2;
9839
+ if (attempt || this.signal?.aborted || planningRetry && this.config.retries === 0 || !planningRetry && !safeToReground(error2, this.signal) && !(!executionStarted && /Stale computer-use observation/.test(String(error2)))) throw error2;
9802
9840
  recovery = redactText(String(error2), this.config.secrets ?? []);
9803
9841
  this.recoveries.push({ kind: planningRetry ? "grounding" : "interaction", reason: recovery, at: (/* @__PURE__ */ new Date()).toISOString() });
9804
9842
  }
9805
9843
  }
9806
9844
  throw new Error("Computer-use grounding exhausted its recovery budget.");
9807
9845
  }
9846
+ observe(page) {
9847
+ return capturePageState(page, this.config.logger, this.config.secrets, this.config.targetPolicy, {
9848
+ timeoutMs: Math.max(1, this.deadline - Date.now()),
9849
+ signal: this.signal,
9850
+ onRetry: (error2) => this.recoveries.push({ kind: "interaction", reason: redactText(String(error2), this.config.secrets ?? []), at: (/* @__PURE__ */ new Date()).toISOString() })
9851
+ });
9852
+ }
9808
9853
  async plan(step, state, system, recovery) {
9809
9854
  const plan = await groundAction({
9810
9855
  ai: this.config.ai,
@@ -9847,7 +9892,7 @@ var BrowserAgent = class {
9847
9892
  const { deadlineMs, cleanDescription } = parseWaitDescription(step.argument);
9848
9893
  const outcome = await pollForPresence({ description: cleanDescription, deadlineMs, pollIntervalMs: 500, check: async () => {
9849
9894
  this.signal?.throwIfAborted();
9850
- const state = await capturePageState(page, this.config.logger, this.config.secrets, this.config.targetPolicy);
9895
+ const state = await this.observe(page);
9851
9896
  const text = await this.config.ai.complete({ system: WAIT_FOR_ELEMENT_SYSTEM, screenshot: state.screenshot, text: redactText(buildWaitForElementPrompt(cleanDescription, state.accessibilityTree), this.config.secrets ?? []) });
9852
9897
  const parsed = extractJSON(text);
9853
9898
  return { present: parsed?.present === true, confidence: typeof parsed?.confidence === "number" ? parsed.confidence : 0, reasoning: typeof parsed?.reasoning === "string" ? parsed.reasoning : void 0 };
@@ -9867,7 +9912,7 @@ var BrowserAgent = class {
9867
9912
  if (passed) this.confirmCandidates(step);
9868
9913
  return passed ? pass(step, start, { resolvedVia: "deterministic" }) : fail(step, start, `Expected exact text "${visible.text}" to be ${visible.visible ? "visible" : "absent"} within ${timeoutMs}ms.`, "assertion", { resolvedVia: "deterministic" });
9869
9914
  }
9870
- const outcome = await evaluateAssertion(this.config.ai, redactText(step.argument, this.config.secrets ?? []), await capturePageState(page, this.config.logger, this.config.secrets, this.config.targetPolicy));
9915
+ const outcome = await evaluateAssertion(this.config.ai, redactText(step.argument, this.config.secrets ?? []), await this.observe(page));
9871
9916
  if (outcome.confidence < 70) throw new Error(`AI could not verify the expected outcome: ${outcome.reasoning}`);
9872
9917
  return outcome.pass ? pass(step, start, { confidence: outcome.confidence, resolvedVia: "ai" }) : fail(step, start, `Assertion failed: ${outcome.reasoning}`, "assertion", { confidence: outcome.confidence, resolvedVia: "ai" });
9873
9918
  }
@@ -9875,7 +9920,7 @@ var BrowserAgent = class {
9875
9920
  let last;
9876
9921
  const performed = /* @__PURE__ */ new Set();
9877
9922
  for (let i = 0; i < 5; i++) {
9878
- const state = await capturePageState(page, this.config.logger, this.config.secrets, this.config.targetPolicy);
9923
+ const state = await this.observe(page);
9879
9924
  const plan = await this.plan(step, state, ACT_SYSTEM);
9880
9925
  const identity = JSON.stringify([plan.action, plan.selector, plan.value]);
9881
9926
  if (performed.has(identity)) return fail(step, start, "The agent requested the same action again without confirmed completion; stopped to avoid duplicate writes.", "interaction");
@@ -9883,7 +9928,7 @@ var BrowserAgent = class {
9883
9928
  this.signal?.throwIfAborted();
9884
9929
  const actionResult = await executeAction(page, plan, { ...this.policy(step), groundedTarget: await resolveVisualTarget(page, plan, state, this.config.secrets ?? [], this.config.targetPolicy) });
9885
9930
  last = pass(step, start, { resolvedVia: "ai", confidence: plan.confidence, _resolvedPlan: plan });
9886
- const after = await capturePageState(page, this.config.logger, this.config.secrets, this.config.targetPolicy);
9931
+ const after = await this.observe(page);
9887
9932
  const response = await this.config.ai.complete({ system: ACT_COMPLETION_SYSTEM, screenshot: after.screenshot, text: redactText(buildCompletionCheckPrompt(step.argument, after.accessibilityTree), this.config.secrets ?? []) });
9888
9933
  if (extractJSON(response)?.complete === true && actionResult.execution?.outcomeObserved) return last;
9889
9934
  if (actionResult.execution?.effect !== "observation" && !actionResult.execution?.outcomeObserved) return fail(step, start, "The action has no independently observed completion. Stopped before another action.", "interaction");
@@ -11499,7 +11544,7 @@ jobs:
11499
11544
  node-version: '22'
11500
11545
  - name: Install Zerocheck
11501
11546
  run: |
11502
- npm install --prefix "$RUNNER_TEMP/zerocheck-cli" --no-save --package-lock=false zerocheck@0.1.2
11547
+ npm install --prefix "$RUNNER_TEMP/zerocheck-cli" --no-save --package-lock=false zerocheck@0.1.3
11503
11548
  echo "$RUNNER_TEMP/zerocheck-cli/node_modules/.bin" >> "$GITHUB_PATH"
11504
11549
  - name: Restore learned targets
11505
11550
  uses: actions/cache@v4
@@ -17532,7 +17577,7 @@ var runner = z2.enum(["local", "hosted"]).default("local").describe("Local brows
17532
17577
  var output = (value) => ({ content: [{ type: "text", text: JSON.stringify(value) }], structuredContent: { result: value } });
17533
17578
  var error = (value) => ({ isError: true, content: [{ type: "text", text: JSON.stringify({ error: "operation_failed", message: value instanceof Error ? value.message : String(value) }) }] });
17534
17579
  function createMcpServer(services) {
17535
- const server = new McpServer({ name: "zerocheck", version: "0.1.2" });
17580
+ const server = new McpServer({ name: "zerocheck", version: "0.1.3" });
17536
17581
  server.registerTool("list_checks", {
17537
17582
  description: "List the repository YAML checks, IDs, exact contents and revisions. Does not execute them.",
17538
17583
  inputSchema: { paths: z2.array(z2.string()).optional(), environment },
@@ -17672,7 +17717,7 @@ async function mcpCommand(options) {
17672
17717
  }
17673
17718
 
17674
17719
  // src/index.ts
17675
- var program = new Command().name("zerocheck").version("0.1.2").description("Turn your team\u2019s manual release checklist into repeatable browser tests.");
17720
+ var program = new Command().name("zerocheck").version("0.1.3").description("Turn your team\u2019s manual release checklist into repeatable browser tests.");
17676
17721
  var directory = (command) => command.option("--project-dir <path>", "Repository project directory", process.cwd());
17677
17722
  var environment2 = (command) => directory(command).addOption(new Option("--env <environment>", "Named project environment").choices(["dev", "staging", "production"]));
17678
17723
  var execution = (command) => environment2(command).addOption(new Option("--runner <runner>", "Browser execution location").choices(["local", "hosted"]).default("local"));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zerocheck",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Turn manual release checklists into repeatable browser tests locally, in CI, or hosted.",
5
5
  "type": "module",
6
6
  "repository": {