peaks-loop 4.0.35 → 4.0.37

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 (128) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README-en.md +1 -1
  3. package/README.md +1 -1
  4. package/bin/peaks.js +71 -1
  5. package/dist/cli/cli-helpers.js +7 -0
  6. package/dist/cli/commands/_register.js +2 -0
  7. package/dist/cli/commands/best-practice-scan-command.d.ts +14 -1
  8. package/dist/cli/commands/best-practice-scan-command.js +67 -9
  9. package/dist/cli/commands/code-runtime-commands.d.ts +5 -2
  10. package/dist/cli/commands/code-runtime-commands.js +78 -7
  11. package/dist/cli/commands/core/doctor-command.d.ts +8 -0
  12. package/dist/cli/commands/core/doctor-command.js +44 -2
  13. package/dist/cli/commands/core/memory-command.js +5 -1
  14. package/dist/cli/commands/dispatch-commands.js +15 -3
  15. package/dist/cli/commands/dispatch-from-dag.js +17 -0
  16. package/dist/cli/commands/hooks-commands.js +10 -1
  17. package/dist/cli/commands/job-commands.js +107 -25
  18. package/dist/cli/commands/memory-commands.d.ts +24 -0
  19. package/dist/cli/commands/memory-commands.js +77 -10
  20. package/dist/cli/commands/request-commands.d.ts +8 -0
  21. package/dist/cli/commands/request-commands.js +23 -2
  22. package/dist/cli/commands/scan-commands.js +1 -1
  23. package/dist/cli/commands/sub-agent-commands.js +2 -0
  24. package/dist/cli/commands/wave-plan-commands.d.ts +24 -0
  25. package/dist/cli/commands/wave-plan-commands.js +93 -0
  26. package/dist/cli/commands/web-commands.d.ts +28 -0
  27. package/dist/cli/commands/web-commands.js +327 -0
  28. package/dist/cli/commands/web-lifecycle-commands.d.ts +49 -0
  29. package/dist/cli/commands/web-lifecycle-commands.js +321 -0
  30. package/dist/services/best-practice/scan-orchestrator.d.ts +22 -0
  31. package/dist/services/best-practice/scan-orchestrator.js +14 -5
  32. package/dist/services/code/orchestrator-can-do.js +27 -4
  33. package/dist/services/context/build-dispatch-system-prompt.d.ts +66 -9
  34. package/dist/services/context/build-dispatch-system-prompt.js +132 -17
  35. package/dist/services/context/context-audit-hint.d.ts +79 -0
  36. package/dist/services/context/context-audit-hint.js +150 -0
  37. package/dist/services/context/context-audit.d.ts +100 -0
  38. package/dist/services/context/context-audit.js +322 -0
  39. package/dist/services/context/summary-view.d.ts +54 -0
  40. package/dist/services/context/summary-view.js +114 -0
  41. package/dist/services/dispatch/file-overlap-wave-planner.d.ts +70 -0
  42. package/dist/services/dispatch/file-overlap-wave-planner.js +119 -0
  43. package/dist/services/dispatch/session-capsule.d.ts +23 -0
  44. package/dist/services/dispatch/session-capsule.js +56 -0
  45. package/dist/services/dispatch/slice-dag.d.ts +9 -0
  46. package/dist/services/dispatch/slice-dag.js +9 -1
  47. package/dist/services/dispatch/test-tool-detection.d.ts +12 -1
  48. package/dist/services/dispatch/test-tool-detection.js +14 -13
  49. package/dist/services/hooks/auto-compact-hook-install.js +10 -1
  50. package/dist/services/hooks/write-gate.js +88 -0
  51. package/dist/services/ide/adapters/claude-code-adapter.d.ts +10 -0
  52. package/dist/services/ide/adapters/claude-code-adapter.js +20 -1
  53. package/dist/services/ide/ide-types.d.ts +15 -0
  54. package/dist/services/lint/detect-eslint.d.ts +2 -0
  55. package/dist/services/lint/detect-eslint.js +23 -9
  56. package/dist/services/lint/npx-resolver.d.ts +6 -0
  57. package/dist/services/lint/npx-resolver.js +38 -14
  58. package/dist/services/memory/project-memory-service/parsers/frontmatter.d.ts +5 -0
  59. package/dist/services/memory/project-memory-service/parsers/frontmatter.js +55 -5
  60. package/dist/services/release/version-precheck-service.js +9 -2
  61. package/dist/services/scan/file-size-scan.d.ts +29 -0
  62. package/dist/services/scan/file-size-scan.js +63 -0
  63. package/dist/services/session/caller-binding-service.d.ts +24 -0
  64. package/dist/services/session/caller-binding-service.js +34 -0
  65. package/dist/services/session/getSessionDir.js +15 -10
  66. package/dist/services/skills/hooks-codegate-superpowers.d.ts +33 -0
  67. package/dist/services/skills/hooks-codegate-superpowers.js +34 -3
  68. package/dist/services/skills/hooks-settings-service.d.ts +10 -0
  69. package/dist/services/skills/hooks-settings-service.js +152 -61
  70. package/dist/services/slice/slice-check-service.d.ts +14 -0
  71. package/dist/services/slice/slice-check-service.js +110 -50
  72. package/dist/services/slice/slice-check-types.d.ts +12 -7
  73. package/dist/services/slice/slice-check-types.js +8 -3
  74. package/dist/services/slice/slice-decompose-runners.js +24 -21
  75. package/dist/services/sop/sop-check-service.js +12 -1
  76. package/dist/services/web/bounded-output.d.ts +34 -0
  77. package/dist/services/web/bounded-output.js +68 -0
  78. package/dist/services/web/browser-acquire.d.ts +14 -0
  79. package/dist/services/web/browser-acquire.js +84 -0
  80. package/dist/services/web/browser-session-manager.d.ts +111 -0
  81. package/dist/services/web/browser-session-manager.js +413 -0
  82. package/dist/services/web/daemon-entry.d.ts +1 -0
  83. package/dist/services/web/daemon-entry.js +65 -0
  84. package/dist/services/web/daemon-registry.d.ts +42 -0
  85. package/dist/services/web/daemon-registry.js +164 -0
  86. package/dist/services/web/daemon-supervisor.d.ts +144 -0
  87. package/dist/services/web/daemon-supervisor.js +455 -0
  88. package/dist/services/web/playwright-loader.d.ts +89 -0
  89. package/dist/services/web/playwright-loader.js +253 -0
  90. package/dist/services/web/snapshot-pruner.d.ts +48 -0
  91. package/dist/services/web/snapshot-pruner.js +241 -0
  92. package/dist/services/web/untrusted-envelope.d.ts +27 -0
  93. package/dist/services/web/untrusted-envelope.js +44 -0
  94. package/dist/services/web/web-artifact-paths.d.ts +79 -0
  95. package/dist/services/web/web-artifact-paths.js +163 -0
  96. package/dist/services/web/web-client.d.ts +19 -0
  97. package/dist/services/web/web-client.js +55 -0
  98. package/dist/services/web/web-daemon-service.d.ts +38 -0
  99. package/dist/services/web/web-daemon-service.js +416 -0
  100. package/dist/services/web/web-fallback.d.ts +70 -0
  101. package/dist/services/web/web-fallback.js +121 -0
  102. package/dist/services/web/web-install-service.d.ts +91 -0
  103. package/dist/services/web/web-install-service.js +346 -0
  104. package/dist/services/web/web-login-profile.d.ts +89 -0
  105. package/dist/services/web/web-login-profile.js +612 -0
  106. package/dist/services/web/web-login-staging.d.ts +27 -0
  107. package/dist/services/web/web-login-staging.js +173 -0
  108. package/dist/services/web/web-protocol.d.ts +58 -0
  109. package/dist/services/web/web-protocol.js +58 -0
  110. package/dist/services/web/web-status-report.d.ts +33 -0
  111. package/dist/services/web/web-status-report.js +47 -0
  112. package/dist/services/workspace/claude-settings-template.d.ts +41 -5
  113. package/dist/services/workspace/claude-settings-template.js +116 -64
  114. package/dist/services/workspace/workspace-claude-settings-materializer.js +5 -1
  115. package/dist/services/workspace/workspace-service.js +33 -0
  116. package/package.json +5 -5
  117. package/scripts/copy-templates.mjs +12 -0
  118. package/scripts/sync-version.mjs +20 -0
  119. package/skills/bee/peaks-qa/SKILL.md +2 -0
  120. package/skills/bee/peaks-qa/references/qa-sub-agent-dispatch.md +12 -0
  121. package/skills/bee/peaks-rd/SKILL.md +2 -0
  122. package/skills/bee/peaks-rd/references/rd-sub-agent-dispatch.md +14 -0
  123. package/skills/bee/peaks-txt/SKILL.md +2 -0
  124. package/skills/bee/peaks-ui/SKILL.md +2 -0
  125. package/skills/peaks-code/SKILL.md +18 -0
  126. package/skills/peaks-code/references/browser-workflow.md +10 -1
  127. package/skills/peaks-code/references/context-governance.md +29 -0
  128. package/skills/peaks-doctor/SKILL.md +2 -0
@@ -19,6 +19,12 @@
19
19
  import { execFileSync } from 'node:child_process';
20
20
  import { existsSync, readFileSync } from 'node:fs';
21
21
  import { join, relative, dirname } from 'node:path';
22
+ import { resolveNpxInvocation } from '../lint/npx-resolver.js';
23
+ // 2026-09-10: the D1 fix (`orchestrator-can-do.ts`) resolved this tree's own CLI
24
+ // entry so a bare `peaks` never had to be resolved through a Windows `.cmd`
25
+ // shim. `runCodegraph` below needs exactly that, so it reuses the same helper
26
+ // rather than growing a second mechanism.
27
+ import { cliEntryPath, interpreterArgs } from '../web/daemon-supervisor.js';
22
28
  export function defaultCodegraphRunner() {
23
29
  return {
24
30
  async query(text, projectRoot) {
@@ -88,34 +94,31 @@ export function defaultCodegraphRunner() {
88
94
  };
89
95
  }
90
96
  function runCodegraph(args, projectRoot) {
97
+ const execOptions = {
98
+ cwd: projectRoot,
99
+ stdio: ['ignore', 'pipe', 'pipe'],
100
+ timeout: 60_000,
101
+ maxBuffer: 32 * 1024 * 1024
102
+ };
91
103
  // Use `peaks codegraph` (the peaks wrapper), which adds --project support.
92
- // Falls back to raw `codegraph` (no --project) if peaks is not on PATH.
93
- const isWin = process.platform === 'win32';
94
- // Try `peaks codegraph` first (the wrapper that understands --project).
104
+ // 2026-09-10: no shell. `peaks` on Windows is a `.cmd` shim, which Node >= 20
105
+ // refuses to spawn without `shell: true` — and `shell: true` concatenates the
106
+ // argv unescaped, so `--project <projectRoot>` was split at the first space in
107
+ // the project path (the same defect that made `peaks slice check` report a
108
+ // phantom failure on such a project). Resolving this tree's own CLI entry and
109
+ // running it through `process.execPath` needs no shim and no shell, so a
110
+ // spaced `projectRoot` is just an argument again.
95
111
  try {
96
- return execFileSync('peaks', ['codegraph', ...args], {
97
- cwd: projectRoot,
98
- stdio: ['ignore', 'pipe', 'pipe'],
99
- shell: isWin,
100
- timeout: 60_000,
101
- maxBuffer: 32 * 1024 * 1024
102
- }).toString('utf8');
112
+ return execFileSync(process.execPath, [...interpreterArgs(cliEntryPath()), 'codegraph', ...args], execOptions).toString('utf8');
103
113
  }
104
114
  catch (error) {
105
115
  const err = error;
106
116
  if (err.code === 'ENOENT') {
107
- // Fallback: raw `codegraph` (won't accept --project, drop it)
117
+ // Fallback: raw `codegraph` (won't accept --project, drop it), reached
118
+ // through the npx resolver so the local `.bin` shim is never spawned.
108
119
  const fallbackArgs = args.filter((a) => a !== '--project' && !a.startsWith('--project='));
109
- const localBin = join(projectRoot, 'node_modules', '.bin', 'codegraph');
110
- const command = existsSync(localBin) ? localBin : 'npx';
111
- const finalArgs = command === 'npx' ? ['codegraph', ...fallbackArgs] : fallbackArgs;
112
- return execFileSync(command, finalArgs, {
113
- cwd: projectRoot,
114
- stdio: ['ignore', 'pipe', 'pipe'],
115
- shell: isWin,
116
- timeout: 60_000,
117
- maxBuffer: 32 * 1024 * 1024
118
- }).toString('utf8');
120
+ const { command, args: npxArgs, baseEnv } = resolveNpxInvocation(['codegraph', ...fallbackArgs]);
121
+ return execFileSync(command, npxArgs, { ...execOptions, env: baseEnv }).toString('utf8');
119
122
  }
120
123
  throw error;
121
124
  }
@@ -118,7 +118,18 @@ function evaluateCommand(projectRoot, run, expectExitZero, allowCommands, timeou
118
118
  }
119
119
  let exitCode;
120
120
  try {
121
- execFileSync(bin, args, { cwd: resolve(projectRoot), timeout: timeoutMs, stdio: 'ignore' });
121
+ // Windows: this spawn is reachable from `peaks gate enforce` (the
122
+ // PreToolUse hook, which forces `allowCommands: true` below), so it runs
123
+ // on a per-Bash-call budget. Without `windowsHide` the child gets its own
124
+ // visible console window on Windows — one window per guarded Bash call.
125
+ // The option is inert on POSIX (libuv reads it only on Windows), which is
126
+ // why it is set unconditionally rather than platform-branched.
127
+ execFileSync(bin, args, {
128
+ cwd: resolve(projectRoot),
129
+ timeout: timeoutMs,
130
+ stdio: 'ignore',
131
+ windowsHide: true
132
+ });
122
133
  exitCode = 0;
123
134
  }
124
135
  catch (error) {
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Byte-aware output caps for `peaks web` (slice S1, AC2).
3
+ *
4
+ * `capText` is the only absolute guarantee in the snapshot pipeline: the
5
+ * pruner (`snapshot-pruner.ts`) bounds node count and depth, but only a byte
6
+ * ceiling bounds the rendered string. These are module constants, not options
7
+ * (tech-doc §12).
8
+ */
9
+ /** Hard ceiling for page text returned by `peaks web text`. */
10
+ export declare const MAX_TEXT_BYTES = 8192;
11
+ /** Hard ceiling for the rendered snapshot returned by `peaks web snap`. */
12
+ export declare const MAX_SNAP_BYTES = 4096;
13
+ /** Maximum number of real (non-marker) nodes emitted by the pruner. */
14
+ export declare const MAX_SNAP_NODES = 120;
15
+ /** Maximum tree depth emitted by the pruner. */
16
+ export declare const MAX_SNAP_DEPTH = 6;
17
+ export interface CappedText {
18
+ readonly text: string;
19
+ readonly truncated: boolean;
20
+ /** UTF-8 bytes removed from the input. `0` when nothing was truncated. */
21
+ readonly droppedBytes: number;
22
+ }
23
+ /**
24
+ * Cut `text` down to at most `capBytes` UTF-8 bytes.
25
+ *
26
+ * Three properties the AC2 tests pin:
27
+ * - the returned string's byte length is **never** greater than `capBytes`
28
+ * (the truncation marker is reserved out of the budget, not added on top);
29
+ * - a multi-byte codepoint is never split (the cut backs off the UTF-8
30
+ * continuation bytes), so the result is always valid UTF-8;
31
+ * - the cut lands on a line boundary when one exists, so the caller never
32
+ * gets a half-line.
33
+ */
34
+ export declare function capText(text: string, capBytes: number): CappedText;
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Byte-aware output caps for `peaks web` (slice S1, AC2).
3
+ *
4
+ * `capText` is the only absolute guarantee in the snapshot pipeline: the
5
+ * pruner (`snapshot-pruner.ts`) bounds node count and depth, but only a byte
6
+ * ceiling bounds the rendered string. These are module constants, not options
7
+ * (tech-doc §12).
8
+ */
9
+ /** Hard ceiling for page text returned by `peaks web text`. */
10
+ export const MAX_TEXT_BYTES = 8192;
11
+ /** Hard ceiling for the rendered snapshot returned by `peaks web snap`. */
12
+ export const MAX_SNAP_BYTES = 4096;
13
+ /** Maximum number of real (non-marker) nodes emitted by the pruner. */
14
+ export const MAX_SNAP_NODES = 120;
15
+ /** Maximum tree depth emitted by the pruner. */
16
+ export const MAX_SNAP_DEPTH = 6;
17
+ /** The marker appended to a truncated payload. Counted INSIDE the ceiling. */
18
+ function truncationMarker(droppedBytes) {
19
+ return `\n…[truncated ${droppedBytes} bytes]`;
20
+ }
21
+ /**
22
+ * Cut `text` down to at most `capBytes` UTF-8 bytes.
23
+ *
24
+ * Three properties the AC2 tests pin:
25
+ * - the returned string's byte length is **never** greater than `capBytes`
26
+ * (the truncation marker is reserved out of the budget, not added on top);
27
+ * - a multi-byte codepoint is never split (the cut backs off the UTF-8
28
+ * continuation bytes), so the result is always valid UTF-8;
29
+ * - the cut lands on a line boundary when one exists, so the caller never
30
+ * gets a half-line.
31
+ */
32
+ export function capText(text, capBytes) {
33
+ const total = Buffer.byteLength(text, 'utf8');
34
+ if (total <= capBytes) {
35
+ return { text, truncated: false, droppedBytes: 0 };
36
+ }
37
+ // Reserve the WIDEST possible marker (the one whose byte count equals the
38
+ // whole input) so the ceiling holds on the first pass: the reported marker
39
+ // can only be narrower than the reserved one.
40
+ const reservedMarkerBytes = Buffer.byteLength(truncationMarker(total), 'utf8');
41
+ if (capBytes < reservedMarkerBytes) {
42
+ // The marker alone would blow the ceiling. The invariant is unconditional,
43
+ // so for a cap too narrow to hold it the whole payload goes.
44
+ return { text: '', truncated: true, droppedBytes: total };
45
+ }
46
+ const kept = cutAtLineBoundary(text, capBytes - reservedMarkerBytes);
47
+ const droppedBytes = total - Buffer.byteLength(kept, 'utf8');
48
+ return { text: kept + truncationMarker(droppedBytes), truncated: true, droppedBytes };
49
+ }
50
+ /**
51
+ * Largest valid-UTF-8 byte prefix of `text` at or below `budget`, backed off
52
+ * to the last line break. `text` without any newline keeps the raw byte cut.
53
+ */
54
+ function cutAtLineBoundary(text, budget) {
55
+ if (budget <= 0) {
56
+ return '';
57
+ }
58
+ const bytes = Buffer.from(text, 'utf8');
59
+ let end = Math.min(budget, bytes.length);
60
+ // A continuation byte (0b10xxxxxx) at the cut point means the codepoint that
61
+ // started earlier would be split, so walk back until it is not one.
62
+ while (end > 0 && ((bytes[end] ?? 0) & 0xc0) === 0x80) {
63
+ end -= 1;
64
+ }
65
+ const prefix = bytes.subarray(0, end).toString('utf8');
66
+ const lastBreak = prefix.lastIndexOf('\n');
67
+ return lastBreak > 0 ? prefix.slice(0, lastBreak) : prefix;
68
+ }
@@ -0,0 +1,14 @@
1
+ import { type PwBrowser } from './playwright-loader.js';
2
+ export interface AcquiredChromium {
3
+ readonly browser: PwBrowser;
4
+ readonly version: string;
5
+ }
6
+ /**
7
+ * The ordered gate: disable → resolve → cache probe → launch-or-refuse.
8
+ *
9
+ * Every exit here is off the download path, by design — see the module
10
+ * docstring. The `CODE: detail` shape is `web-daemon-service`'s
11
+ * `failureResponse` convention, so the caller gets `WEB_INSTALL_REQUIRED` and
12
+ * not a collapsed `WEB_OP_FAILED`.
13
+ */
14
+ export declare function acquireChromium(): Promise<AcquiredChromium>;
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Acquire the chromium browser (slice S1, file 10; S3 adds the ordered gate).
3
+ *
4
+ * The order is load-bearing and is the whole of AC5's first half (tech-doc §5.1):
5
+ *
6
+ * 1. `PEAKS_WEB_DISABLED=1` → refuse, BEFORE any resolve, any lock file and
7
+ * any cache touch. A gate evaluated after a spawn is not a gate.
8
+ * 2. resolve the pinned Playwright (no npx, no shell — `playwright-loader`).
9
+ * 3. probe the cache. This is spawn-free and download-free (R6): it answers
10
+ * about the artifact `launchOnce` actually starts — the headless shell —
11
+ * so "is an install needed" never costs 700 MB to ask, and never answers
12
+ * about a browser this code will not launch.
13
+ * 4. launch, or refuse with `WEB_INSTALL_REQUIRED`.
14
+ *
15
+ * **Step 4 is a refusal, not a download** (R3). Installing here meant a blocking
16
+ * `spawnSync` on the daemon's request loop for up to 20 minutes: `/health`
17
+ * starved, `status` reported the daemon `orphaned`, `stop` could not prove
18
+ * ownership and left it running, and the CLI gave up at 30 s and called the op
19
+ * failed while the download carried on invisibly. The download now belongs to
20
+ * `peaks web install`, which runs in the caller's own process, on the caller's
21
+ * terminal, and prints its size before it blocks. The daemon answers the op with
22
+ * the tier-3 envelope instead.
23
+ *
24
+ * We never delete anything in the Playwright cache; recovery is delegated to
25
+ * Playwright's own `install --force` (design §10.1).
26
+ */
27
+ import { getErrorMessage } from 'peaks-loop-shared/result';
28
+ import { loadPlaywright } from './playwright-loader.js';
29
+ import { installCommandLine, isWebDisabled, probeBrowserInstalled } from './web-install-service.js';
30
+ /** The message Playwright raises when the browser binary was never downloaded. */
31
+ const MISSING_EXECUTABLE_RE = /Executable doesn't exist/i;
32
+ /**
33
+ * The ordered gate: disable → resolve → cache probe → launch-or-refuse.
34
+ *
35
+ * Every exit here is off the download path, by design — see the module
36
+ * docstring. The `CODE: detail` shape is `web-daemon-service`'s
37
+ * `failureResponse` convention, so the caller gets `WEB_INSTALL_REQUIRED` and
38
+ * not a collapsed `WEB_OP_FAILED`.
39
+ */
40
+ export async function acquireChromium() {
41
+ assertWebEnabled();
42
+ const playwright = await loadPlaywright();
43
+ const probe = await probeBrowserInstalled();
44
+ if (!probe.installed) {
45
+ throw new Error(`WEB_INSTALL_REQUIRED: ${installRequiredDetail(probe.version)}`);
46
+ }
47
+ try {
48
+ return await launchOnce(playwright);
49
+ }
50
+ catch (error) {
51
+ if (!MISSING_EXECUTABLE_RE.test(getErrorMessage(error))) {
52
+ throw error;
53
+ }
54
+ // The probe named an existing executable that launch still would not take —
55
+ // a half-written or corrupt revision. `--force` is the documented recovery
56
+ // (R6), and the caller has to run it: this process must not download.
57
+ throw new Error(`WEB_INSTALL_REQUIRED: --force ${installRequiredDetail(probe.version)}`);
58
+ }
59
+ }
60
+ /** One sentence, shared by both refusal paths, naming the command that fixes it. */
61
+ function installRequiredDetail(version) {
62
+ const held = version === null ? 'the pinned Playwright is not resolvable' : `playwright@${version} is cached`;
63
+ return (`${held} but its browser is not launched-ready; run \`peaks web install\` ` +
64
+ `(\`npx ${installCommandLine().join(' ')}\`)`);
65
+ }
66
+ /** Step 1. `PEAKS_WEB_DISABLED=1` means no browser, no spawn, no cache touch. */
67
+ function assertWebEnabled() {
68
+ if (isWebDisabled(process.env)) {
69
+ throw new Error('WEB_DISABLED: PEAKS_WEB_DISABLED=1 — the local browser path is switched off for this process');
70
+ }
71
+ }
72
+ /**
73
+ * Launch with no options: `headless` defaults true and Playwright resolves
74
+ * `chromium-headless-shell` (`registry.getExecutableName`). That is deliberate —
75
+ * the full `chromium` build under new-headless mode was measured on this machine
76
+ * at **15 101 ms to `close()`**, against **102 ms** for the shell, and S2's
77
+ * teardown has to finish inside `STOP_EXIT_TIMEOUT_MS = 10 s` or the daemon is
78
+ * SIGTERM'd mid-teardown and its browser is orphaned (AC6). `probeBrowserInstalled`
79
+ * is what had to change, not this.
80
+ */
81
+ async function launchOnce(playwright) {
82
+ const browser = await playwright.chromium.launch();
83
+ return { browser, version: browser.version() };
84
+ }
@@ -0,0 +1,111 @@
1
+ import type { PwBrowser, PwContext } from './playwright-loader.js';
2
+ export interface BrowserSessionManagerOptions {
3
+ readonly projectRoot: string;
4
+ readonly sessionId: string;
5
+ }
6
+ export interface WebTextResult {
7
+ readonly text: string;
8
+ readonly truncated: boolean;
9
+ readonly droppedBytes: number;
10
+ }
11
+ export interface WebSnapResult {
12
+ readonly snapshot: string;
13
+ readonly droppedNodes: number;
14
+ readonly depthCapped: boolean;
15
+ readonly nodeCapped: boolean;
16
+ readonly truncated: boolean;
17
+ readonly droppedBytes: number;
18
+ }
19
+ export interface WebMetricsResult {
20
+ readonly available: boolean;
21
+ readonly reason: string | null;
22
+ readonly values: Readonly<Record<string, unknown>> | null;
23
+ }
24
+ /** One dispatch whose storage state could not be persisted at teardown. */
25
+ export interface StateWriteFailure {
26
+ readonly dispatchId: string;
27
+ readonly reason: string;
28
+ }
29
+ /**
30
+ * Resolve with `work`, or reject once the step has taken longer than the step
31
+ * budget. The late settlement of `work` is consumed here, so a step that
32
+ * finishes after its deadline cannot become an unhandled rejection.
33
+ */
34
+ export declare function boundedTeardownStep<T>(work: Promise<T>, label: string): Promise<T>;
35
+ export interface CloseAllResult {
36
+ /** Contexts closed during this call. */
37
+ readonly closedContexts: number;
38
+ /** Never silent: teardown still proceeds, but the caller can report this. */
39
+ readonly stateWriteFailures: readonly StateWriteFailure[];
40
+ }
41
+ export declare class BrowserSessionManager {
42
+ private readonly browser;
43
+ private readonly projectRoot;
44
+ private readonly sessionId;
45
+ private readonly sessions;
46
+ constructor(browser: PwBrowser, options: BrowserSessionManagerOptions);
47
+ /**
48
+ * The context for a dispatch, created on first use and reused afterwards.
49
+ *
50
+ * `profile` names a user-level login profile (`peaks web login --profile`) to
51
+ * build the context from. When given it REPLACES the dispatch's own state: a
52
+ * context holds one storage state, so "start from the profile" and "start from
53
+ * the per-dispatch state" are alternatives, not layers.
54
+ *
55
+ * The profile is **READ-ONLY**. This module reads it into the context and
56
+ * never writes, refreshes or trims it — write-back is an undecided design
57
+ * question (it would keep a login fresh without re-running `login`, and it
58
+ * would also let an automated browse silently rewrite a user-level credential
59
+ * file), so this slice does not decide it. `closeAll` still persists the
60
+ * dispatch's OWN `pw-profiles/<dispatchId>/storageState.json`; nothing under
61
+ * `~/.peaks/web-profiles/` is ever created or modified from here.
62
+ *
63
+ * The name is validated HERE and not only in the CLI, because it arrives off
64
+ * the wire: `resolveProfileName` is the SAME resolver `login` uses (folds to
65
+ * lower case, refuses a traversing name, a leading dot, a device name), and
66
+ * the read is anchored with `assertUnder`. A profile that does not exist is a
67
+ * NAMED failure — falling through to an unauthenticated context would answer
68
+ * "open this with my profile" with a logged-out browser and no signal.
69
+ */
70
+ contextFor(dispatchId: string, rawProfile?: string): Promise<PwContext>;
71
+ open(dispatchId: string, url: string, profile?: string): Promise<{
72
+ url: string;
73
+ title: string;
74
+ }>;
75
+ text(dispatchId: string, selector?: string): Promise<WebTextResult>;
76
+ /**
77
+ * Primary path is `locator.ariaSnapshotJSON()` (Playwright 1.63, our exact
78
+ * pin). We do NOT re-derive the tree from the YAML form: parsing YAML by
79
+ * indentation is the fragile option, and under an exact pin the JSON API is
80
+ * always present (tech-doc §4.1/§4.2). A missing API is an explicit error
81
+ * rather than a silent degradation.
82
+ */
83
+ snap(dispatchId: string, selector?: string): Promise<WebSnapResult>;
84
+ click(dispatchId: string, selector: string): Promise<{
85
+ result: string;
86
+ }>;
87
+ /**
88
+ * Screenshot to an absolute path under `web/`. Always an explicit `path`:
89
+ * never the Playwright default, and never a Buffer we write ourselves
90
+ * (tech-doc §7.2 rule 3).
91
+ */
92
+ shot(dispatchId: string, selector?: string): Promise<{
93
+ path: string;
94
+ bytes: number;
95
+ }>;
96
+ /**
97
+ * Core Web Vitals for the dispatch's page. Returns `available: false` with a
98
+ * reason — never fabricated zeros — when there is no observation window
99
+ * (orchestrator decision C4), and never echoes the page's object back: only
100
+ * the three contract keys, only finite numbers (R2).
101
+ */
102
+ metrics(dispatchId: string): Promise<WebMetricsResult>;
103
+ /**
104
+ * Persist each dispatch's storage state, then close every context exactly
105
+ * once. A state file that cannot be written must not block shutdown — but it
106
+ * must be reported: a silent skip here kills S4's persistence with no signal
107
+ * anywhere (R1).
108
+ */
109
+ closeAll(): Promise<CloseAllResult>;
110
+ private pageFor;
111
+ }