@mjasnikovs/pi-task 0.38.3 → 0.38.4

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.
@@ -1,3 +1,4 @@
1
+ import WebSocket from 'ws';
1
2
  export type DeepRenderOutcome = {
2
3
  outcome: 'pass';
3
4
  detail: string;
@@ -132,6 +133,31 @@ export declare function deriveLegacyFacts(log: SessionRequest[]): Pick<DeepSessi
132
133
  export declare function judgeDeepSession(f: DeepSessionFacts): DeepRenderOutcome;
133
134
  /** Whole-session wall-clock cap, including browser launch (I4). */
134
135
  export declare const DEEP_RENDER_TIMEOUT_MS = 45000;
136
+ /** Minimal DevTools-protocol client: request/response ids over one socket, plus
137
+ * event fan-out. Everything the driver needs and nothing more.
138
+ *
139
+ * @internal Exported for its own tests: the id/response matching and the
140
+ * send-failure path are the parts of the driver that need no browser. */
141
+ export declare class Cdp {
142
+ private readonly ws;
143
+ private nextId;
144
+ private readonly pending;
145
+ private readonly handlers;
146
+ constructor(ws: WebSocket);
147
+ on(method: string, cb: (params: Record<string, unknown>) => void): void;
148
+ send(method: string, params?: Record<string, unknown>, sessionId?: string): Promise<Record<string, unknown>>;
149
+ }
150
+ /** Page-side fill: native value setters + input/change events, so a controlled
151
+ * React/Vue/Svelte input actually updates its state (a bare `el.value = …` does
152
+ * not, and the form then submits empty).
153
+ *
154
+ * @internal Exported so the credential ESCAPING is testable: a password with a
155
+ * quote or a backslash in it must not break out of the injected expression. */
156
+ export declare function fillExpr(identifier: string, password: string): string;
157
+ /** Wait until `ms` of silence pass with no new request, or `cap` elapses.
158
+ * @internal Exported for its own tests — the quiet/cap arithmetic decides
159
+ * whether a slow page is judged half-loaded. */
160
+ export declare function settle(lastActivity: () => number, cap: number, quiet?: number): Promise<void>;
135
161
  /**
136
162
  * Drive one authenticated session against `url` and judge it. `cwd` is the project
137
163
  * whose dotenv declares the account. Every failure mode of the DRIVER itself
@@ -146,4 +172,8 @@ export declare function runDeepRenderCheck(url: string, cwd: string, opts?: {
146
172
  /** Recorder hook: receives the facts the verdict was made on. Used by the
147
173
  * corpus builder; the gate itself never passes it. */
148
174
  onFacts?: (f: DeepSessionFacts) => void;
175
+ /** @internal Settle quiet window, for tests only. The three settle windows
176
+ * cost QUIET_MS each against a fake browser that answers instantly, which
177
+ * is the whole runtime of a driver test. The gate never passes it. */
178
+ quietMs?: number;
149
179
  }): Promise<DeepRenderOutcome>;
@@ -348,8 +348,11 @@ const POST_SUBMIT_CAP_MS = 12_000;
348
348
  * small so the three settle windows together stay inside DEEP_RENDER_TIMEOUT_MS. */
349
349
  const RE_NAV_CAP_MS = 6_000;
350
350
  /** Minimal DevTools-protocol client: request/response ids over one socket, plus
351
- * event fan-out. Everything the driver needs and nothing more. */
352
- class Cdp {
351
+ * event fan-out. Everything the driver needs and nothing more.
352
+ *
353
+ * @internal Exported for its own tests: the id/response matching and the
354
+ * send-failure path are the parts of the driver that need no browser. */
355
+ export class Cdp {
353
356
  ws;
354
357
  nextId = 1;
355
358
  pending = new Map();
@@ -418,8 +421,11 @@ const INSPECT_EXPR = `(() => {
418
421
  })()`;
419
422
  /** Page-side fill: native value setters + input/change events, so a controlled
420
423
  * React/Vue/Svelte input actually updates its state (a bare `el.value = …` does
421
- * not, and the form then submits empty). */
422
- function fillExpr(identifier, password) {
424
+ * not, and the form then submits empty).
425
+ *
426
+ * @internal Exported so the credential ESCAPING is testable: a password with a
427
+ * quote or a backslash in it must not break out of the injected expression. */
428
+ export function fillExpr(identifier, password) {
423
429
  return `(() => {
424
430
  const setValue = (el, v) => {
425
431
  const proto = el instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype
@@ -457,8 +463,10 @@ const SUBMIT_EXPR = `(() => {
457
463
  if (submit) { submit.click(); return {ok: true, how: 'click'} }
458
464
  return {ok: false, reason: 'no submit control'}
459
465
  })()`;
460
- /** Wait until `ms` of silence pass with no new request, or `cap` elapses. */
461
- async function settle(lastActivity, cap, quiet = QUIET_MS) {
466
+ /** Wait until `ms` of silence pass with no new request, or `cap` elapses.
467
+ * @internal Exported for its own tests — the quiet/cap arithmetic decides
468
+ * whether a slow page is judged half-loaded. */
469
+ export async function settle(lastActivity, cap, quiet = QUIET_MS) {
462
470
  const deadline = Date.now() + cap;
463
471
  for (;;) {
464
472
  const idleFor = Date.now() - lastActivity();
@@ -514,7 +522,7 @@ export async function runDeepRenderCheck(url, cwd, opts = {}) {
514
522
  child = c;
515
523
  }, s => {
516
524
  socket = s;
517
- }, opts.onFacts), budget);
525
+ }, opts.onFacts, opts.quietMs), budget);
518
526
  }
519
527
  catch (e) {
520
528
  const why = e instanceof Error ? e.message : String(e);
@@ -540,7 +548,7 @@ function withTimeout(p, ms) {
540
548
  });
541
549
  });
542
550
  }
543
- async function drive(url, bin, userDataDir, credentials, holdChild, holdSocket, onFacts) {
551
+ async function drive(url, bin, userDataDir, credentials, holdChild, holdSocket, onFacts, quietMs) {
544
552
  const origin = new URL(url).origin;
545
553
  /** Every verdict goes through here, so a recorder sees the same facts the judge
546
554
  * does — the corpus is what the gate itself read, not a reconstruction. */
@@ -626,7 +634,7 @@ async function drive(url, bin, userDataDir, credentials, holdChild, holdSocket,
626
634
  return r.result?.value;
627
635
  };
628
636
  await cdp.send('Page.navigate', { url }, sessionId);
629
- await settle(() => lastActivity, SETTLE_CAP_MS);
637
+ await settle(() => lastActivity, SETTLE_CAP_MS, quietMs);
630
638
  const before = await evaluate(INSPECT_EXPR);
631
639
  if (!before)
632
640
  throw new Error('the page could not be inspected');
@@ -698,7 +706,7 @@ async function drive(url, bin, userDataDir, credentials, holdChild, holdSocket,
698
706
  if (!submitted?.ok)
699
707
  return judge(unsubmitted({ submitted: false }));
700
708
  lastActivity = Date.now();
701
- await settle(() => lastActivity, POST_SUBMIT_CAP_MS);
709
+ await settle(() => lastActivity, POST_SUBMIT_CAP_MS, quietMs);
702
710
  // The sign-in request: the first same-origin non-GET issued by the submit. Its
703
711
  // own 2xx is the precondition for judging anything, and it is excluded from the
704
712
  // data evidence (STEP 0: the broken build satisfies "≥1 same-origin 2xx" with
@@ -731,7 +739,7 @@ async function drive(url, bin, userDataDir, credentials, holdChild, holdSocket,
731
739
  if (authAccepted && leftAuthWall) {
732
740
  await cdp.send('Page.navigate', { url }, sessionId);
733
741
  lastActivity = Date.now();
734
- await settle(() => lastActivity, RE_NAV_CAP_MS);
742
+ await settle(() => lastActivity, RE_NAV_CAP_MS, quietMs);
735
743
  }
736
744
  return judge(facts(sessionLog(authId, authAt), {
737
745
  submitted: true,
@@ -38,6 +38,19 @@ export declare function collectChangedFiles(cwd: string, signal?: AbortSignal):
38
38
  * never a blocker. The `.pi-tasks/` bookkeeping is excluded from every git command.
39
39
  */
40
40
  export declare function collectAddedLines(cwd: string, signal?: AbortSignal): Promise<AddedLine[]>;
41
+ /**
42
+ * Deterministic sandbox-path-leak pass (see foreign-path.ts, mx5 run 13 PROMPT 4
43
+ * item 1): find absolute paths the task committed that resolve nowhere on this
44
+ * machine while the real file sits in the repo, REPAIR the ones whose relative
45
+ * form provably resolves, and return verify findings for whatever is left.
46
+ *
47
+ * The repair runs here, before the verify child, for the same reason lint-fix
48
+ * does: the defect is mechanical and the correct target is already known, so
49
+ * spending an AUTOFIX round (or a human) on a path substitution is waste. What it
50
+ * cannot repair still reaches the child under rule 4e. Failures degrade to no
51
+ * findings — a sharpener, never a blocker.
52
+ */
53
+ export declare function collectForeignPathFindings(cwd: string, signal?: AbortSignal, logDebug?: (m: string) => void): Promise<string[]>;
41
54
  /**
42
55
  * Deterministic neutered-check-script pass (see script-escape.ts, mx5 run 13 PROMPT
43
56
  * 4 item 4): check-class scripts that cannot report failure, in a manifest THIS
@@ -61,6 +61,18 @@ export function truncateToolResult(text, limit = TOOL_RESULT_LOG_LIMIT) {
61
61
  }
62
62
  /** Keep the gate machinery's own artifacts out of every git pathspec below. */
63
63
  const EXCLUDE_TASKS_DIR = ':(exclude).pi-tasks';
64
+ /**
65
+ * Pin the diff header prefixes on any command whose output we PARSE for paths.
66
+ *
67
+ * `parseAddedLines` reads the file out of the `+++ b/…` header, but the prefix is
68
+ * user-configurable: `diff.mnemonicPrefix=true` emits `i/` and `w/` instead of
69
+ * `a/` and `b/`, and `diff.noprefix=true` emits none. Both are common developer
70
+ * settings, and under either one every added line was attributed to a path like
71
+ * `w/src/app.ts`, which exists nowhere — so the probe-gaming and sandbox-path
72
+ * probes silently read a diff of files that are not in the repo. Passing the
73
+ * prefixes explicitly makes the output independent of the host's git config.
74
+ */
75
+ const DIFF_PREFIX_ARGS = ['--src-prefix=a/', '--dst-prefix=b/'];
64
76
  const splitLines = (s) => s
65
77
  .split('\n')
66
78
  .map(l => l.trim())
@@ -110,7 +122,7 @@ export async function collectChangedFiles(cwd, signal) {
110
122
  * never a blocker. The `.pi-tasks/` bookkeeping is excluded from every git command.
111
123
  */
112
124
  export async function collectAddedLines(cwd, signal) {
113
- const tracked = await git(cwd, ['diff', 'HEAD', '--', '.', EXCLUDE_TASKS_DIR], signal);
125
+ const tracked = await git(cwd, ['diff', ...DIFF_PREFIX_ARGS, 'HEAD', '--', '.', EXCLUDE_TASKS_DIR], signal);
114
126
  const lines = tracked.exitCode === 0 ? parseAddedLines(tracked.stdout) : [];
115
127
  const untracked = await git(cwd, ['ls-files', '--others', '--exclude-standard', '--', '.', EXCLUDE_TASKS_DIR], signal);
116
128
  for (const name of splitLines(untracked.exitCode === 0 ? untracked.stdout : '')) {
@@ -124,7 +136,7 @@ export async function collectAddedLines(cwd, signal) {
124
136
  }
125
137
  }
126
138
  if (lines.length === 0) {
127
- const last = await git(cwd, ['diff', 'HEAD~1..HEAD', '--', '.', EXCLUDE_TASKS_DIR], signal);
139
+ const last = await git(cwd, ['diff', ...DIFF_PREFIX_ARGS, 'HEAD~1..HEAD', '--', '.', EXCLUDE_TASKS_DIR], signal);
128
140
  return last.exitCode === 0 ? parseAddedLines(last.stdout) : [];
129
141
  }
130
142
  return lines;
@@ -141,7 +153,7 @@ export async function collectAddedLines(cwd, signal) {
141
153
  * cannot repair still reaches the child under rule 4e. Failures degrade to no
142
154
  * findings — a sharpener, never a blocker.
143
155
  */
144
- async function collectForeignPathFindings(cwd, signal, logDebug) {
156
+ export async function collectForeignPathFindings(cwd, signal, logDebug) {
145
157
  const lines = await collectAddedLines(cwd, signal);
146
158
  if (lines.length === 0)
147
159
  return [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mjasnikovs/pi-task",
3
- "version": "0.38.3",
3
+ "version": "0.38.4",
4
4
  "description": "Deterministic task planning and spec-orchestration for local models — crash-safe /task pipelines with verify/enforce gates, a real-time remote web view, and web/docs/fetch/worker subagent tools.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",