pi-ui-extend 1.0.20 → 1.0.21

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.
@@ -251,9 +251,11 @@ The built-in `browser-qa` role runs on `antigravity/gemini-3-flash-preview`, wit
251
251
  `openai-codex/gpt-5.4-mini` as its fallback. Its browser workflow is an explicit
252
252
  private skill under `src/async-subagents/private-skills/`, outside normal Pi skill
253
253
  discovery. The role's first-class `isolatedSkills` setting launches the child with
254
- `--no-skills` plus the private workflow and the packaged `skills/playwright-cli`
255
- skill. Both are mandatory even when configuration adds other isolated skills;
256
- parent and ordinary sub-agent sessions do not discover the private workflow.
254
+ `--no-skills` plus one self-contained private workflow. It bundles the relevant
255
+ scenario-design, locator, waiting, assertion, evidence, and cleanup guidance next
256
+ to its trusted runner, so browser QA does not depend on a separately installed
257
+ skill or CLI. The private workflow remains mandatory when configuration appends
258
+ other isolated skills; parent and ordinary sub-agent sessions do not discover it.
257
259
 
258
260
  Keep named dev/staging auth profiles in project `.pi/qa_auth.jsonc` (there is no
259
261
  `/qa-auth` command). The private runner supports `form`, `cookie`, `localStorage`,
@@ -288,13 +290,18 @@ blocked. Example:
288
290
 
289
291
  Do not place credential values in prompts, QA flows, shell arguments, reports,
290
292
  or evidence. The helper reads JSONC internally, emits only redacted statuses, and
291
- caches generated storage state under `.pi/qa-auth-state`. Evidence is isolated by
292
- run/profile under `.pi/qa-runs`; trace archives have network records and non-image
293
- resources removed, then known configured/runtime credential values are redacted
294
- and verified before the trace is retained. Missing, ambiguous, rejected, or
295
- expired auth returns `QA_PROFILE_REQUIRED` or `QA_AUTH_UPDATE_REQUIRED`, naming
296
- only the profile/file/reason needed for the parent to ask the user for an update
297
- and rerun. See
293
+ caches generated storage state under `.pi/qa-auth-state`. The launcher provides
294
+ each browser QA process with its own
295
+ `.pi/subagents/<run>/<agent-id>/browser-qa/` workspace. Declarative flows,
296
+ screenshots, video, sanitized traces, and result manifests stay there, so normal
297
+ session shutdown or `subagents cleanup` removes them with the run directory.
298
+ The runner validates the owning agent metadata and refuses flows outside that
299
+ workspace; reusing an agent id clears stale browser QA files first. Trace archives
300
+ have network records and non-image resources removed, then known
301
+ configured/runtime credential values are redacted and verified before retention.
302
+ Missing, ambiguous, rejected, or expired auth returns `QA_PROFILE_REQUIRED` or
303
+ `QA_AUTH_UPDATE_REQUIRED`, naming only the profile/file/reason needed for the
304
+ parent to ask the user for an update and rerun. See
298
305
  `src/async-subagents/private-skills/browser-qa/references/qa-auth.example.jsonc`
299
306
  for complete profile shapes and `references/qa-flow.example.jsonc` beside it for
300
307
  the declarative, non-executable QA action/assertion format.
@@ -17,9 +17,10 @@ evidence. The role uses `antigravity/gemini-3-flash-preview`, falling back to
17
17
  Antigravity-backed role unavailable.
18
18
  - A type profile may declare `isolatedSkills`. Spawning that profile adds
19
19
  `--no-skills` followed by one explicit `--skill` per configured path.
20
- - The `browser-qa` profile always loads both its private workflow and the
21
- packaged `skills/playwright-cli` skill. Configuration may append isolated
22
- skills but cannot remove either mandatory browser skill.
20
+ - The `browser-qa` profile always loads one self-contained private workflow.
21
+ Relevant browser-test design guidance is bundled beside its trusted runner;
22
+ no separately discovered skill or browser CLI is required. Configuration may
23
+ append isolated skills but cannot remove the mandatory private workflow.
23
24
  - Other sub-agent profiles and the parent session must not discover the private
24
25
  skill automatically.
25
26
 
@@ -40,9 +41,11 @@ evidence. The role uses `antigravity/gemini-3-flash-preview`, falling back to
40
41
  - The bundled runner reads secrets internally. Credentials must never be copied
41
42
  into prompts, generated QA flows, shell arguments, transcripts, reports,
42
43
  or QA evidence.
43
- - Generated browser state is private cache under `.pi/qa-auth-state`; evidence
44
- is written under `.pi/qa-runs`. Multiple profiles always use separate browser
45
- contexts and evidence directories.
44
+ - Generated browser state is private cache under `.pi/qa-auth-state`. Ephemeral
45
+ flows, evidence, and result manifests are written under the owning agent's
46
+ `.pi/subagents/<run>/<agent-id>/browser-qa/` workspace. Multiple profiles use
47
+ separate browser contexts/evidence directories, and normal sub-agent shutdown
48
+ or cleanup removes the whole workspace with its run.
46
49
  - Missing, ambiguous, rejected, or expired auth returns a machine-readable
47
50
  update-required/profile-required status naming only the profile id, config
48
51
  file, and redacted reason. The parent asks the user to update the file and
@@ -54,6 +57,10 @@ evidence. The role uses `antigravity/gemini-3-flash-preview`, falling back to
54
57
  trusted runner implements a bounded set of navigation, interaction,
55
58
  assertion, screenshot, and auth-rejection actions and never gives the flow a
56
59
  Playwright context or credential values.
60
+ - The launcher injects `PI_SUBAGENT_AGENT_DIR`, pre-creates a private
61
+ `browser-qa/flows/` workspace, and clears stale browser QA files when an agent
62
+ id is reused. The runner validates the directory's project/type metadata and
63
+ refuses flows outside it; the model cannot select a shared evidence root.
57
64
  - The runner owns browser lifecycle, origin checks, auth application, tracing,
58
65
  screenshots, video finalization, and redacted result output. Before retaining
59
66
  a trace it removes network/non-image resource entries, redacts configured and
@@ -69,10 +76,10 @@ evidence. The role uses `antigravity/gemini-3-flash-preview`, falling back to
69
76
 
70
77
  ## Acceptance criteria
71
78
 
72
- 1. `browser-qa` resolves to the intended model/fallback, private workflow, and
73
- packaged `playwright-cli` skill, and its isolated child process can register
74
- the configured Antigravity model.
75
- 2. Spawn args contain `--no-skills` and both mandatory explicit skills for this
79
+ 1. `browser-qa` resolves to the intended model/fallback and its self-contained
80
+ private workflow, and its isolated child process can register the configured
81
+ Antigravity model.
82
+ 2. Spawn args contain `--no-skills` and the mandatory private skill for this
76
83
  profile; ordinary profiles retain existing skill discovery behavior.
77
84
  3. Auth profile listing and all error output are redacted; model-authored input
78
85
  cannot execute code in the credential-bearing process.
@@ -80,9 +87,11 @@ evidence. The role uses `antigravity/gemini-3-flash-preview`, falling back to
80
87
  origins, path/mode hardening, private empty-template creation, explicit
81
88
  credential requests, non-executable flows, and successful redacted evidence
82
89
  creation.
83
- 5. Completed test runs report clickable screenshot, video, and trace links
90
+ 5. Browser QA flows/evidence live only inside the owning sub-agent directory;
91
+ deleting the run removes them while persistent auth config/state remains.
92
+ 6. Completed test runs report clickable screenshot, video, and trace links
84
93
  whenever those artifacts exist.
85
- 6. Suite tests/typecheck, host checks, and suite sync pass.
94
+ 7. Suite tests/typecheck, host checks, and suite sync pass.
86
95
 
87
96
  ## Real-browser regression test
88
97
 
@@ -96,6 +105,8 @@ npm run test:browser-qa-e2e
96
105
  ```
97
106
 
98
107
  Normal suite tests keep this case skipped; the Publish workflow runs it on
99
- Linux after installing Chromium. Explicit runs retain the latest artifacts in
100
- `.pi/qa-runs/browser-qa-e2e/latest/` and print a clickable link for every file.
101
- Set `BROWSER_QA_KEEP_EVIDENCE=0` to clean up without publishing evidence.
108
+ Linux after installing Chromium. The runner writes into a temporary simulated
109
+ sub-agent directory. For manual inspection only, explicit E2E runs copy the
110
+ latest artifacts to `.pi/qa-runs/browser-qa-e2e/latest/` and print clickable
111
+ links; this test-only published copy is not the runtime storage contract. Set
112
+ `BROWSER_QA_KEEP_EVIDENCE=0` to skip that copy.
@@ -288,21 +288,6 @@ export function getBrowserQaSkillPath(): string {
288
288
  return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "private-skills", "browser-qa", "SKILL.md");
289
289
  }
290
290
 
291
- export function getPlaywrightCliSkillPath(): string {
292
- const bundledPath = path.resolve(
293
- path.dirname(fileURLToPath(import.meta.url)),
294
- "../../../../..",
295
- "skills",
296
- "playwright-cli",
297
- "SKILL.md",
298
- );
299
- const installedPaths = [
300
- path.join(os.homedir(), ".agents", "skills", "playwright-cli", "SKILL.md"),
301
- path.join(os.homedir(), ".pi", "agent", "skills", "playwright-cli", "SKILL.md"),
302
- ];
303
- return [bundledPath, ...installedPaths].find((candidate) => fs.existsSync(candidate)) ?? bundledPath;
304
- }
305
-
306
291
  export function getSubagentConfigInitTargetPath(cwd: string, env: NodeJS.ProcessEnv = process.env): string {
307
292
  return explicitSubagentConfigPath(cwd, env) ?? getDefaultSubagentConfigPath();
308
293
  }
@@ -417,7 +402,7 @@ export function resolveAgentTaskConfig(
417
402
  function resolveIsolatedSkills(selectedType: string | undefined, profile: SubagentTypeConfig | undefined): string[] {
418
403
  const configured = arrayOfStrings(profile?.isolatedSkills) ?? [];
419
404
  if (selectedType !== "browser-qa") return configured;
420
- return [...new Set([getBrowserQaSkillPath(), getPlaywrightCliSkillPath(), ...configured])];
405
+ return [...new Set([getBrowserQaSkillPath(), ...configured])];
421
406
  }
422
407
 
423
408
  export function resolveSubagentRoutingConfig(config: SubagentConfig): ResolvedSubagentRoutingConfig {
@@ -24,6 +24,8 @@ export interface SpawnAgentOptions {
24
24
  }
25
25
 
26
26
  export const DEFAULT_AGENT_TIMEOUT_MS = 30 * 60 * 1000;
27
+ const BROWSER_QA_WORKSPACE_DIR = "browser-qa";
28
+ const SUBAGENT_AGENT_DIR_ENV = "PI_SUBAGENT_AGENT_DIR";
27
29
  const AGENT_TIMEOUT_EXIT_CODE = 124;
28
30
  const AGENT_TIMEOUT_KILL_GRACE_MS = 5_000;
29
31
  const AGENT_SETTLED_TERMINATE_GRACE_MS = 50;
@@ -46,6 +48,7 @@ export function spawnAgent(
46
48
  validateBasename(task.id, "task.id");
47
49
  const agentDir = path.join(runDir, task.id);
48
50
  fs.mkdirSync(agentDir, { recursive: true });
51
+ prepareBrowserQaWorkspace(agentDir, task.subagentType);
49
52
 
50
53
  // Clean previous state when reusing a run directory/agent id.
51
54
  for (const f of [
@@ -136,7 +139,7 @@ export function spawnAgent(
136
139
 
137
140
  const proc = spawn(invocation.command, invocation.args, {
138
141
  cwd,
139
- env: subagentEnvironment(process.env),
142
+ env: subagentEnvironment(process.env, task.subagentType === "browser-qa" ? agentDir : undefined),
140
143
  stdio: ["pipe", "pipe", "pipe"],
141
144
  });
142
145
  proc.stdin.on("error", (error: NodeJS.ErrnoException) => {
@@ -698,8 +701,8 @@ function getEnvModel(): string | undefined {
698
701
  return trimmed ? trimmed : undefined;
699
702
  }
700
703
 
701
- function subagentEnvironment(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
702
- return {
704
+ function subagentEnvironment(env: NodeJS.ProcessEnv, agentDir?: string): NodeJS.ProcessEnv {
705
+ const result: NodeJS.ProcessEnv = {
703
706
  ...env,
704
707
  PI_MODEL_SUITABLE_TOOLS_PRESERVE_SELECTION: "1",
705
708
  PI_TERMINAL_BELL_DISABLED: "1",
@@ -709,6 +712,21 @@ function subagentEnvironment(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
709
712
  "question",
710
713
  ]),
711
714
  };
715
+ delete result[SUBAGENT_AGENT_DIR_ENV];
716
+ if (agentDir) result[SUBAGENT_AGENT_DIR_ENV] = fs.realpathSync(agentDir);
717
+ return result;
718
+ }
719
+
720
+ function prepareBrowserQaWorkspace(agentDir: string, subagentType: string | undefined): void {
721
+ const workspace = path.join(agentDir, BROWSER_QA_WORKSPACE_DIR);
722
+ fs.rmSync(workspace, { recursive: true, force: true });
723
+ if (subagentType !== "browser-qa") return;
724
+ const flows = path.join(workspace, "flows");
725
+ fs.mkdirSync(flows, { recursive: true, mode: 0o700 });
726
+ if (process.platform !== "win32") {
727
+ fs.chmodSync(workspace, 0o700);
728
+ fs.chmodSync(flows, 0o700);
729
+ }
712
730
  }
713
731
 
714
732
  function appendEnvList(value: string | undefined, items: readonly string[]): string {
@@ -24,7 +24,6 @@ export {
24
24
  existingSubagentConfigFiles,
25
25
  getDefaultSubagentConfigPath,
26
26
  getBrowserQaSkillPath,
27
- getPlaywrightCliSkillPath,
28
27
  getSubagentConfigInitTargetPath,
29
28
  getSubagentConfigSamplePath,
30
29
  isBlindModelRef,
@@ -1,31 +1,71 @@
1
1
  ---
2
2
  name: browser-qa-private
3
- description: Private workflow for deterministic browser bug reproduction and fix verification with redacted project auth and Playwright evidence.
3
+ description: Private self-contained workflow for deterministic browser bug reproduction and fix verification with redacted project auth and Playwright evidence.
4
4
  ---
5
5
 
6
6
  # Browser QA
7
7
 
8
- Use the bundled runner; do not read, print, grep, copy, or edit credential values
9
- from `.pi/qa_auth.jsonc` yourself.
8
+ Use this skill's bundled runner as the only browser interface. It already owns
9
+ Playwright, browser/context lifecycle, tracing, video, screenshots, origin
10
+ isolation, authentication, redaction, and cleanup. Do not invoke another browser
11
+ CLI, create shared/default browser sessions, or generate executable browser code.
12
+
13
+ Never read, print, grep, copy, or edit credential values from
14
+ `.pi/qa_auth.jsonc` yourself.
10
15
 
11
16
  ## Workflow
12
17
 
13
18
  1. Resolve `scripts/browser-qa-runner.mjs` relative to this skill.
14
- 2. Run `node <runner> profiles`. Choose a profile only when the task names its
19
+ 2. Use the launcher-provided `$PI_SUBAGENT_AGENT_DIR/browser-qa/` workspace.
20
+ The launcher creates its private `flows/` directory and the runner rejects
21
+ flows or evidence destinations outside this owning sub-agent directory. Do
22
+ not override `PI_SUBAGENT_AGENT_DIR` or copy evidence to shared project paths.
23
+ 3. Discover the requested target, expected behavior, and the smallest scenario
24
+ that can prove it. If the target cannot be reached or started, report the
25
+ concrete blocker instead of substituting static checks for browser QA.
26
+ 4. Run `node <runner> profiles`. Choose a profile only when the task names its
15
27
  id or safe profile traits make the choice unambiguous. Otherwise stop with
16
28
  `QA_PROFILE_REQUIRED` and list only ids, descriptions, and traits.
17
- 3. Inspect the target code and write a declarative JSONC flow under
18
- `.pi/qa-flows/`. Never put credentials or executable JavaScript in it.
19
- 4. Run:
29
+ 5. Inspect the target code and write a declarative JSONC flow under
30
+ `$PI_SUBAGENT_AGENT_DIR/browser-qa/flows/`. Never put credentials or
31
+ executable JavaScript in it.
32
+ 6. Run:
20
33
  `node <runner> run --profile <id> --base-url <url> --flow <flow.jsonc>`.
21
34
  Profile id, URL, and flow path are non-secret; never pass credentials as
22
35
  arguments or environment variables.
23
- 5. Report deterministic assertions and every artifact returned by the runner.
36
+ 7. Report deterministic assertions and every artifact returned by the runner.
24
37
  For each screenshot, video, or trace, emit a separate clickable Markdown
25
38
  link using its `uri` and also show its absolute `path`. Do this for failed
26
39
  runs too whenever `artifacts` is present; never report only `evidenceDir`.
27
40
  Visual inspection supplements assertions; it does not replace them.
28
41
 
42
+ ## Scenario design
43
+
44
+ - Define the expected postcondition before writing interactions. A successful
45
+ click or navigation is not proof; assert the resulting URL, text, value,
46
+ count, visibility, enabled state, or checked state.
47
+ - Keep the flow minimal and reproducible. Capture setup, the action under test,
48
+ and at least one observable outcome; add a screenshot at the state that best
49
+ explains the result.
50
+ - Prefer stable user-facing locators in this order: `testId`; semantic `role`
51
+ plus accessible `name`; `label`; `placeholder`; visible `text`; CSS only as a
52
+ last resort. Use `exact: true` when similar elements could make a match
53
+ ambiguous.
54
+ - Let locator actions auto-wait. Use `waitFor` for an explicit UI state and use
55
+ `waitForTimeout` only for a short, unavoidable animation/debounce—not as a
56
+ substitute for an assertion. Set flow `timeoutMs` only as high as the target
57
+ legitimately needs.
58
+ - Place `authRejectedIf` immediately after navigation or any transition that
59
+ may reveal expired authentication.
60
+ - Never weaken an assertion merely to make a failing run pass. If the observed
61
+ product behavior differs from the expectation, preserve the failure evidence
62
+ and report the mismatch.
63
+
64
+ Read `references/qa-design.md` when designing a non-trivial flow, diagnosing an
65
+ ambiguous failure, or deciding what evidence proves the result.
66
+
67
+ ## Flow contract
68
+
29
69
  The flow is `{ "steps": [...] }` with at most 100 steps. Supported actions:
30
70
 
31
71
  - navigation: `goto`, `reload`, `waitFor`, `waitForTimeout`
@@ -50,16 +90,21 @@ assertions require exactly one of `equals` or `includes`.
50
90
  "locator": { "role": "heading", "name": "Settings", "exact": true }
51
91
  },
52
92
  { "action": "click", "locator": { "testId": "save-settings" } },
53
- { "action": "assertText", "locator": { "testId": "toast" }, "includes": "Saved" }
93
+ { "action": "assertText", "locator": { "testId": "toast" }, "includes": "Saved" },
94
+ { "action": "screenshot", "name": "settings-saved" }
54
95
  ]
55
96
  }
56
97
  ```
57
98
 
58
- The runner owns Playwright/browser lifecycle, strict network-origin boundaries,
59
- auth application, assertions, screenshots, video, trace sanitization, and
60
- cleanup. Do not start an additional shared/default browser session. For multiple
61
- profiles, invoke the runner separately; each invocation gets an isolated context
62
- and exclusive evidence directory.
99
+ For multiple profiles, invoke the runner separately. Every invocation gets an
100
+ isolated browser context and exclusive evidence directory; the runner closes
101
+ all owned browser resources on success and failure. Flows, screenshots, video,
102
+ sanitized traces, and runner result manifests remain under
103
+ `$PI_SUBAGENT_AGENT_DIR/browser-qa/` so normal sub-agent shutdown or cleanup
104
+ deletes them with the run directory. `.pi/qa_auth.jsonc` and reusable
105
+ `.pi/qa-auth-state/` remain project-local and persistent.
106
+
107
+ ## Credentials and blocked runs
63
108
 
64
109
  If the runner returns `QA_AUTH_UPDATE_REQUIRED`, stop and explicitly report that
65
110
  browser QA requires credentials or an auth-config update. Ask the user to fill
@@ -68,9 +113,13 @@ empty template was created at that path. Relay only the runner's profile, file,
68
113
  reason, action, and template-created state; never read the generated file or
69
114
  attempt to recover by exposing or replaying credentials.
70
115
 
116
+ For any other blocked run, report the runner status and redacted reason. Do not
117
+ claim that browser QA passed based on source inspection, unit tests, or a build.
118
+
71
119
  After any runner invocation that actually performed browser testing, include
72
120
  all non-empty `artifacts.screenshots`, `artifacts.videos`, and
73
121
  `artifacts.traces` groups in the final response. These links are mandatory so
74
122
  the user can open the evidence directly.
75
123
 
76
- See `references/qa-auth.example.jsonc` and `references/qa-flow.example.jsonc`.
124
+ See `references/qa-auth.example.jsonc`, `references/qa-flow.example.jsonc`, and
125
+ `references/qa-design.md`.
@@ -0,0 +1,106 @@
1
+ # Designing deterministic browser QA flows
2
+
3
+ Use this reference with the bundled declarative runner. It intentionally does
4
+ not describe a separate browser CLI or executable Playwright scripts.
5
+
6
+ ## Build the proof before the steps
7
+
8
+ Write down three things first:
9
+
10
+ 1. **Setup:** the page and state needed to expose the behavior.
11
+ 2. **Action:** the smallest user interaction that exercises it.
12
+ 3. **Oracle:** the observable state that proves success or reproduces failure.
13
+
14
+ Good oracles are product-visible and specific: an exact URL, a stable status
15
+ message, a field value, item count, enabled/disabled state, or checked state.
16
+ Avoid treating “the click did not throw” or “the screenshot looks plausible” as
17
+ proof.
18
+
19
+ When verifying a fix, prefer a focused regression flow over a broad tour of the
20
+ application. If multiple independent states matter, assert each one explicitly.
21
+
22
+ ## Choose resilient locators
23
+
24
+ Prefer locators that match how users and accessibility APIs identify controls:
25
+
26
+ 1. `testId` when the product exposes a stable test contract.
27
+ 2. `role` with accessible `name` for buttons, links, headings, dialogs, and
28
+ similar semantic elements.
29
+ 3. `label` for form controls.
30
+ 4. `placeholder` or visible `text` when they are stable product copy.
31
+ 5. `css` only when no semantic contract exists.
32
+
33
+ Use `exact: true` when duplicate or substring matches are possible. Avoid CSS
34
+ that encodes DOM depth, generated classes, styling details, or element order.
35
+ If a locator is ambiguous, inspect nearby source or rendered copy and choose a
36
+ more specific product contract rather than adding arbitrary delays.
37
+
38
+ ## Wait for state, not time
39
+
40
+ Runner interactions inherit Playwright auto-waiting. Usually an action followed
41
+ by an assertion is enough. Use `waitFor` only when the next operation depends on
42
+ a distinct attached/detached/visible/hidden transition.
43
+
44
+ `waitForTimeout` is bounded to five seconds and should be exceptional—for a
45
+ known animation, debounce, or externally scheduled transition with no
46
+ observable intermediate state. Sleeping longer hides races instead of proving
47
+ behavior. If a normal operation legitimately needs more time, adjust the flow's
48
+ `timeoutMs` rather than inserting repeated sleeps.
49
+
50
+ ## Authentication transitions
51
+
52
+ Add `authRejectedIf` directly after initial navigation and after transitions
53
+ that can redirect to login or display an expired-session marker. This converts
54
+ stale credentials into an explicit update request instead of misreporting a
55
+ product regression.
56
+
57
+ Do not encode credentials, tokens, storage values, or login form secrets in the
58
+ flow. The trusted runner applies the selected profile internally and removes
59
+ secret-bearing evidence if it detects disclosure.
60
+
61
+ ## Evidence strategy
62
+
63
+ The runner always attempts a final or failure screenshot, records video, and
64
+ creates a sanitized trace once the browser launches. Add named `screenshot`
65
+ steps only at states that materially help explain the result—for example before
66
+ and after a destructive interaction, or when a transient success message is
67
+ the oracle.
68
+
69
+ Use evidence by purpose:
70
+
71
+ - **Screenshot:** quick review of one meaningful visual state.
72
+ - **Video:** chronological confirmation of the complete user flow.
73
+ - **Trace:** action/DOM timing diagnosis for a failed or flaky interaction.
74
+
75
+ Assertions determine pass/fail; evidence explains it. Preserve and link every
76
+ artifact group returned on both passed and failed runs.
77
+
78
+ ## Diagnose failures without weakening the test
79
+
80
+ Classify the first failing step:
81
+
82
+ - wrong target/setup or service unavailable;
83
+ - authentication rejected or expired;
84
+ - locator no longer matches the product contract;
85
+ - expected state never appeared;
86
+ - actual product behavior contradicts the expectation.
87
+
88
+ Fix the flow only when its setup or locator is wrong. Do not replace a precise
89
+ assertion with a vague one, increase timeouts reflexively, or remove the failing
90
+ step to manufacture a pass. Keep the failure artifacts and state the expected
91
+ versus observed behavior.
92
+
93
+ ## Cleanup and isolation
94
+
95
+ Each runner invocation owns one isolated context and evidence directory and
96
+ closes its browser resources in a `finally` path. Do not create parallel shared
97
+ or default sessions outside the runner. Test multiple auth profiles with
98
+ separate invocations so cookies, storage, traces, and evidence cannot mix.
99
+
100
+ Keep the declarative flow and every generated screenshot, video, trace, and
101
+ result manifest inside `$PI_SUBAGENT_AGENT_DIR/browser-qa/`. The launcher owns
102
+ that path and the runner validates it before opening a browser. Do not override
103
+ the environment path or copy evidence into shared `.pi/qa-runs`/`.pi/qa-flows`
104
+ directories: the agent-local workspace is intentionally removed by the normal
105
+ sub-agent shutdown and cleanup lifecycle. Authentication config and reusable
106
+ auth state are separate persistent inputs and stay under project `.pi/`.
@@ -10,7 +10,9 @@ import { strFromU8, strToU8, unzipSync, zipSync } from "../vendor/fflate.mjs";
10
10
 
11
11
  const CONFIG_RELATIVE = ".pi/qa_auth.jsonc";
12
12
  const STATE_RELATIVE = path.join(".pi", "qa-auth-state");
13
- const RUNS_RELATIVE = path.join(".pi", "qa-runs");
13
+ const SUBAGENT_AGENT_DIR_ENV = "PI_SUBAGENT_AGENT_DIR";
14
+ const QA_WORKSPACE_RELATIVE = "browser-qa";
15
+ const EVIDENCE_RELATIVE = "evidence";
14
16
  const EXIT_AUTH_UPDATE_REQUIRED = 42;
15
17
  const EXIT_PROFILE_REQUIRED = 43;
16
18
  const PROFILE_ID = /^[A-Za-z0-9._-]+$/;
@@ -80,23 +82,25 @@ async function main() {
80
82
  const profile = selected.profile;
81
83
  const secrets = collectSecrets(profile.auth);
82
84
  try {
83
- await runQa({ cwd, args, profileId, profile });
85
+ const agentDir = resolveBrowserQaAgentDirectory(cwd, process.env[SUBAGENT_AGENT_DIR_ENV]);
86
+ await runQa({ cwd, agentDir, args, profileId, profile });
84
87
  } catch (error) {
85
88
  if (error instanceof QaStatusError) throw error;
86
89
  throw new QaStatusError("QA_RUN_FAILED", redact(safeReason(error), secrets), 1, profileId);
87
90
  }
88
91
  }
89
92
 
90
- async function runQa({ cwd, args, profileId, profile }) {
93
+ async function runQa({ cwd, agentDir, args, profileId, profile }) {
91
94
  if (!args.flow) throw new QaStatusError("QA_RUN_FAILED", "--flow is required", 1, profileId);
92
- const flowPath = resolveExistingPrivateFile(cwd, args.flow, "QA flow", false);
95
+ const workspaceDir = path.join(agentDir, QA_WORKSPACE_RELATIVE);
96
+ const flowPath = resolveExistingPrivateFile(workspaceDir, args.flow, "QA flow", false);
93
97
  const flow = readFlow(flowPath, profileId);
94
98
  const allowedOrigins = normalizeAllowedOrigins(profile.allowedOrigins, profileId);
95
99
  const baseURL = normalizeBaseUrl(args.baseUrl ?? profile.baseUrl ?? allowedOrigins[0], allowedOrigins, profileId);
96
100
  validateAuthConfiguration(cwd, profile.auth, allowedOrigins, profileId);
97
101
  const runId = safeRunId(args.runId ?? `${timestamp()}-${profileId}`);
98
- const evidenceDir = path.join(cwd, RUNS_RELATIVE, runId, profileId);
99
- createExclusivePrivateDirectory(cwd, evidenceDir);
102
+ const evidenceDir = path.join(workspaceDir, EVIDENCE_RELATIVE, runId, profileId);
103
+ createExclusivePrivateDirectory(agentDir, evidenceDir);
100
104
 
101
105
  const playwright = loadPlaywright(cwd);
102
106
  const browser = await playwright.chromium.launch({ headless: true });
@@ -779,6 +783,45 @@ function isAllowedUrl(raw, allowedOrigins) {
779
783
  }
780
784
  }
781
785
 
786
+ function resolveBrowserQaAgentDirectory(cwd, value) {
787
+ if (typeof value !== "string" || value.length === 0) {
788
+ throw new Error(`${SUBAGENT_AGENT_DIR_ENV} is required for browser QA runs`);
789
+ }
790
+ const projectRoot = fs.realpathSync(cwd);
791
+ const subagentRoot = path.join(projectRoot, ".pi", "subagents");
792
+ const resolved = path.resolve(value);
793
+ if (!fs.existsSync(resolved)) throw new Error("browser QA agent directory is missing");
794
+ const real = fs.realpathSync(resolved);
795
+ if (!isInside(subagentRoot, real)) throw new Error("browser QA agent directory must be inside .pi/subagents");
796
+ assertNoSymlinkComponents(projectRoot, real, "browser QA agent directory");
797
+ if (!fs.statSync(real).isDirectory()) throw new Error("browser QA agent directory must be a real directory");
798
+
799
+ const promptFile = path.join(real, "prompt.md");
800
+ const projectFile = path.join(real, "project_cwd");
801
+ const typeFile = path.join(real, "subagent_type");
802
+ for (const [file, label] of [[promptFile, "prompt"], [projectFile, "project metadata"], [typeFile, "type metadata"]]) {
803
+ if (!fs.existsSync(file) || !fs.statSync(file).isFile()) throw new Error(`browser QA ${label} is missing`);
804
+ assertNoSymlinkComponents(real, file, `browser QA ${label}`);
805
+ }
806
+ const recordedProject = fs.readFileSync(projectFile, "utf8").trim();
807
+ if (!recordedProject || fs.realpathSync(recordedProject) !== projectRoot) {
808
+ throw new Error("browser QA agent directory belongs to another project");
809
+ }
810
+ if (fs.readFileSync(typeFile, "utf8").trim() !== "browser-qa") {
811
+ throw new Error("browser QA runner requires a browser-qa sub-agent directory");
812
+ }
813
+
814
+ const workspace = path.join(real, QA_WORKSPACE_RELATIVE);
815
+ if (!fs.existsSync(workspace)) throw new Error("browser QA workspace is missing");
816
+ assertNoSymlinkComponents(real, workspace, "browser QA workspace");
817
+ const workspaceStat = fs.statSync(workspace);
818
+ if (!workspaceStat.isDirectory()) throw new Error("browser QA workspace must be a real directory");
819
+ if (process.platform !== "win32" && (workspaceStat.mode & 0o077) !== 0) {
820
+ throw new Error("browser QA workspace must use private directory permissions (0700)");
821
+ }
822
+ return real;
823
+ }
824
+
782
825
  function resolveExistingPrivateFile(cwd, value, label, requirePrivate = true) {
783
826
  if (typeof value !== "string" || value.length === 0) throw new Error(`${label} path is missing`);
784
827
  const root = fs.realpathSync(cwd);
@@ -794,9 +837,13 @@ function resolveExistingPrivateFile(cwd, value, label, requirePrivate = true) {
794
837
  return real;
795
838
  }
796
839
 
797
- function assertInside(root, target, label) {
840
+ function isInside(root, target) {
798
841
  const relative = path.relative(root, target);
799
- if (!relative || (!relative.startsWith("..") && !path.isAbsolute(relative))) return;
842
+ return !relative || (!relative.startsWith("..") && !path.isAbsolute(relative));
843
+ }
844
+
845
+ function assertInside(root, target, label) {
846
+ if (isInside(root, target)) return;
800
847
  throw new Error(`${label} must be project-local`);
801
848
  }
802
849
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-ui-extend",
3
- "version": "1.0.20",
3
+ "version": "1.0.21",
4
4
  "description": "Pix: a workspace-first terminal UI for Pi with tabs, readable tool activity, voice input, and bundled agent tools.",
5
5
  "private": false,
6
6
  "repository": {