Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,33 @@ on:
pull_request:

jobs:
dependency-audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Scan dependency lockfiles
uses: google/osv-scanner-action/osv-scanner-action@9a498708959aeaef5ef730655706c5a1df1edbc2
with:
scan-args: |-
scan source
--recursive
./

gitleaks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Scan Git history for secrets
run: |
docker run --rm \
-v "$PWD:/repo" \
ghcr.io/gitleaks/gitleaks:v8.30.1 \
git --redact --no-banner /repo

test:
runs-on: ubuntu-latest
steps:
Expand All @@ -23,6 +50,8 @@ jobs:

- run: bun run typecheck

- run: bun run lint

- name: Install desktop test dependencies
run: |
sudo apt-get update
Expand Down
7 changes: 7 additions & 0 deletions .gitleaksignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Synthetic secret fixtures retained in Git history; tracked in pickforge/picklab#60.
de467fbd14d00d4afa4946f610d05d724a6fa777:packages/browser/test/evidence-integration.test.ts:jwt:27
d3e4353d8fbd470deaaab30c61c7fdb0c5b14727:packages/browser/test/evidence-integration.test.ts:jwt:27
596c7c21cd5472c99db0f516988b5483354e2fe7:packages/core/test/redact.test.ts:jwt:258
596c7c21cd5472c99db0f516988b5483354e2fe7:packages/core/test/evidence-sanitize.test.ts:jwt:12
d00b06b3e14aaf1a29a7fc7d9e271619f2d6c41a:packages/cli/test/telemetry.test.ts:github-pat:83
8bc348cfb7cc88f9e276a7102c063642609f2d18:packages/cli/test/telemetry.test.ts:github-pat:83
159 changes: 152 additions & 7 deletions bun.lock

Large diffs are not rendered by default.

13 changes: 9 additions & 4 deletions docs/releases/UNRELEASED.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,10 @@ release description, then reset it after the release is published.

## Internal/release changes

- Added blocking TypeScript complexity caps, ratcheted Vitest coverage floors,
Git-history secret scanning, dependency vulnerability scanning, and required
branch-protection checks for the complete CI gate baseline. Legacy complexity
and scanner exceptions remain explicitly tracked in pickforge/picklab#60.
- Added a single storage resolver (`resolveRunStorage`) covering home,
project-local, and custom modes with stable per-project id derivation
(sha256 of the canonical project path); routed run creation, the run
Expand Down Expand Up @@ -178,6 +182,11 @@ release description, then reset it after the release is published.

- `bun install --frozen-lockfile`
- `bun run typecheck`
- `bun run lint`
- `bun run test:coverage` in a local Linux container (1,129 passed, 5 skipped;
82.47% lines and 86.13% branches)
- `gitleaks git --redact --no-banner .`
- `osv-scanner scan source --recursive .`
- Focused run catalog, run, evidence, CLI artifact, and MCP resource/tool suites.
- New `storage.test.ts` (project id derivation, all three storage modes, env
overrides) and `run-catalog.test.ts`'s "openRunCatalog storage modes" suite
Expand Down Expand Up @@ -264,10 +273,6 @@ release description, then reset it after the release is published.

### Not tested yet

- `bun run test:coverage` on a machine where the full suite runs clean (this
dev sandbox can't produce a coverage summary because the pre-existing
Darwin-only failures above stop the process before the v8 coverage
provider flushes it — reproduced identically on unmodified `main`).
- Platform smoke checks outside Linux.
- Real `sudo -A -v` graphical-prompt smoke on a live Linux desktop (approve
and cancel) — the automated suite covers this with a stand-in `sudo`/
Expand Down
31 changes: 31 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import tseslint from "typescript-eslint";

export default [
{
ignores: ["coverage/**", "packages/*/dist/**"],
},
{
files: ["**/*.ts"],
languageOptions: {
parser: tseslint.parser,
},
rules: {
complexity: ["error", 15],
"max-depth": ["error", 4],
"max-lines-per-function": [
"error",
{
max: 100,
skipBlankLines: true,
skipComments: true,
},
],
},
},
{
files: ["**/*.test.ts", "test/**/*.ts"],
rules: {
"max-lines-per-function": "off",
},
},
];
7 changes: 7 additions & 0 deletions osv-scanner.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[[IgnoredVulns]]
id = "GHSA-frvp-7c67-39w9"
reason = "Below the high/critical gate; the fixed @hono/node-server release requires an upstream major-version update. Tracked in pickforge/picklab#60."

[[IgnoredVulns]]
id = "GHSA-g7r4-m6w7-qqqr"
reason = "Below the high/critical gate; the fixed esbuild release is outside current direct dependency ranges. Tracked in pickforge/picklab#60."
8 changes: 8 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,14 @@
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=23.0.0"
},
"overrides": {
"body-parser": "2.3.0",
"fast-uri": "3.1.4",
"hono": "4.12.27"
},
"scripts": {
"build": "node scripts/build.mjs",
"lint": "eslint .",
"test": "vitest run",
"test:coverage": "vitest run --coverage",
"test:live:android": "PICKLAB_LIVE_ANDROID=1 vitest run packages/android/test/integration.test.ts",
Expand All @@ -18,8 +24,10 @@
"devDependencies": {
"@types/node": "^24.0.0",
"@vitest/coverage-v8": "3.2.6",
"eslint": "10.7.0",
"tsup": "^8.5.0",
"typescript": "^5.9.0",
"typescript-eslint": "8.65.0",
"vitest": "3.2.6"
}
}
2 changes: 2 additions & 0 deletions packages/android/src/emulator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ export function pickConsolePort(usedSerials: readonly string[]): number {
);
}

// eslint-disable-next-line complexity -- Legacy gate debt: pickforge/picklab#60
export async function waitForBoot(opts: WaitForBootOptions): Promise<void> {
assertSerial(opts.serial);
const timeoutMs = opts.timeoutMs ?? DEFAULT_BOOT_TIMEOUT_MS;
Expand Down Expand Up @@ -373,6 +374,7 @@ export async function stopEmulator(
return stopped;
}

// eslint-disable-next-line complexity -- Legacy gate debt: pickforge/picklab#60
async function stopEmulatorProcess(
opts: StopEmulatorOptions,
): Promise<boolean> {
Expand Down
3 changes: 3 additions & 0 deletions packages/browser/src/devtools-evidence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ function structuredContent(message: JsonRpcMessage): Record<string, unknown> | u
: undefined;
}

// eslint-disable-next-line complexity -- Legacy gate debt: pickforge/picklab#60
function diagnosticActions(
message: JsonRpcMessage,
sessionId: string,
Expand Down Expand Up @@ -396,6 +397,7 @@ async function captureInlinePng(
return path.join("screenshots", filename);
}

// eslint-disable-next-line max-lines-per-function -- Legacy gate debt: pickforge/picklab#60
export async function createDevtoolsEvidenceRecorder(
opts: CreateDevtoolsEvidenceRecorderOptions,
): Promise<DevtoolsEvidenceRecorder | undefined> {
Expand Down Expand Up @@ -452,6 +454,7 @@ export async function createDevtoolsEvidenceRecorder(
report(error);
}
},
// eslint-disable-next-line complexity -- Legacy gate debt: pickforge/picklab#60
afterResponse: async (message) => {
try {
const id = requestId(message);
Expand Down
1 change: 1 addition & 0 deletions packages/browser/src/devtools-mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,7 @@ async function stopChild(
return exit;
}

// eslint-disable-next-line max-lines-per-function, complexity -- Legacy gate debt: pickforge/picklab#60
export async function runDevtoolsMcpRelay(
opts: RunDevtoolsMcpRelayOptions,
): Promise<RelayExit> {
Expand Down
2 changes: 2 additions & 0 deletions packages/browser/src/ndjson.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ function isJsonRpcId(value: unknown): value is JsonRpcId {
);
}

// eslint-disable-next-line complexity -- Legacy gate debt: pickforge/picklab#60
export function assertJsonRpcMessage(value: unknown): asserts value is JsonRpcMessage {
if (!isObject(value) || value.jsonrpc !== "2.0") {
throw new Error('expected a JSON-RPC 2.0 object with jsonrpc: "2.0"');
Expand Down Expand Up @@ -333,6 +334,7 @@ export interface PumpJsonRpcNdjsonOptions {

const runWriteImmediately: JsonRpcWriteSerializer = (write) => write();

// eslint-disable-next-line complexity -- Legacy gate debt: pickforge/picklab#60
export async function pumpJsonRpcNdjson(
source: Readable,
destination: Writable,
Expand Down
3 changes: 3 additions & 0 deletions packages/browser/src/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,7 @@ async function stopOwnedBrowserDaemon(
* If cleanup cannot be confirmed, the error record retains every known
* identity and is marked for a later reaper retry.
*/
// eslint-disable-next-line max-lines-per-function, complexity -- Legacy gate debt: pickforge/picklab#60
export async function createBrowserSession(
opts: CreateBrowserSessionOptions,
): Promise<BrowserSessionHandle> {
Expand Down Expand Up @@ -576,6 +577,7 @@ export async function getBrowserSessionStatus(
* into one error
* and leave the record in `error` state for inspection.
*/
// eslint-disable-next-line max-lines-per-function -- Legacy gate debt: pickforge/picklab#60
export async function teardownBrowserSession(
id: string,
registryEnv: EnvLike,
Expand All @@ -589,6 +591,7 @@ export async function teardownBrowserSession(
throw new Error(`Session ${id} is not a browser session`);
}

// eslint-disable-next-line max-lines-per-function, complexity -- Legacy gate debt: pickforge/picklab#60
await withSessionVncLock(id, registryEnv, async () => {
const record = await getSession(id, registryEnv);
if (record === undefined) {
Expand Down
1 change: 1 addition & 0 deletions packages/browser/test/session.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -809,6 +809,7 @@ describe.skipIf(!hasXvfb)("partial-failure cleanup (fake binaries)", () => {
expect(await getSession(id, registryEnv)).toBeUndefined();
}, TEST_TIMEOUT_MS);

// eslint-disable-next-line complexity -- Legacy gate debt: pickforge/picklab#60
it("preserves retry state or completes cleanup across the supervisor reap race", async () => {
const sessionsPath = path.join(home, "sessions");
fs.mkdirSync(sessionsPath, { recursive: true });
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/commands/android.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,7 @@ export async function runAndroidLogcat(
});
}

// eslint-disable-next-line complexity -- Legacy gate debt: pickforge/picklab#60
export async function runAndroidAdb(
args: string[],
opts: AndroidTargetOptions,
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ function planLabUserProvisioning(
});
}

// eslint-disable-next-line max-lines-per-function, complexity -- Legacy gate debt: pickforge/picklab#60
export async function runInit(
opts: InitCliOptions,
env: EnvLike = process.env,
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/commands/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ function describeCreated(summary: LocalSessionSummary): string {
export async function runSessionCreate(
opts: SessionCreateOptions,
): Promise<number> {
// eslint-disable-next-line complexity -- Legacy gate debt: pickforge/picklab#60
return runReported(opts, async () => {
const projectDir = resolveProjectDir(opts);
const config = await loadConfig(projectDir);
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ function withAndroidTarget(command: Command): Command {
);
}

// eslint-disable-next-line max-lines-per-function -- Legacy gate debt: pickforge/picklab#60
export function buildProgram(): Command {
const program = new Command()
.name("picklab")
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/provision/checks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ function pathCheck(
return { id, title, status: missingStatus, detail: "not found", hint };
}

// eslint-disable-next-line max-lines-per-function -- Legacy gate debt: pickforge/picklab#60
export function evaluateChecks(s: DetectionSnapshot): DoctorCheck[] {
const checks: DoctorCheck[] = [];

Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/provision/detect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ export async function labUserExists(
}
}

// eslint-disable-next-line complexity -- Legacy gate debt: pickforge/picklab#60
export async function collectSnapshot(
opts: CollectSnapshotOptions = {},
): Promise<DetectionSnapshot> {
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/provision/executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,7 @@ function prepareSections(
});
}

// eslint-disable-next-line max-lines-per-function, complexity -- Legacy gate debt: pickforge/picklab#60
export async function executeProvisioning(
sections: readonly ProvisioningSection[],
opts: ExecuteProvisioningOptions = {},
Expand Down Expand Up @@ -457,6 +458,7 @@ export async function executeProvisioning(
skipped.push(decision.reason);
} else {
addError(decision.reason, decision.kind);
// eslint-disable-next-line max-depth -- Legacy gate debt: pickforge/picklab#60
if (section.consent.retainPlanOnDenied === true) {
selected.push(...entry.steps);
}
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/evidence-sanitize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ export interface SanitizedNetworkFailure {
* timing, and a sanitized error summary. Headers, bodies, and query strings
* have no field here and can never be persisted through this shape.
*/
// eslint-disable-next-line complexity -- Legacy gate debt: pickforge/picklab#60
export function sanitizeNetworkFailure(input: {
method?: string;
url?: string;
Expand Down
7 changes: 7 additions & 0 deletions packages/core/src/evidence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -615,6 +615,7 @@ async function withJournalLock<T>(
* just-created run is finalized (`failed`) and the claim released, so no
* permanent running orphan is ever left behind.
*/
// eslint-disable-next-line max-lines-per-function, complexity -- Legacy gate debt: pickforge/picklab#60
export async function beginEvidenceRun(
projectDir: string,
sessionId: string,
Expand Down Expand Up @@ -655,7 +656,9 @@ export async function beginEvidenceRun(
// between the wx create and its identity stamp).
const owner = resolution.claim;
if (owner !== undefined) {
// eslint-disable-next-line max-depth -- Legacy gate debt: pickforge/picklab#60
if (identityIsAlive(owner.ownerPid, owner.ownerStartTicks)) {
// eslint-disable-next-line max-depth -- Legacy gate debt: pickforge/picklab#60
if (Date.now() >= deadline) break;
await delay(claimBackoff(attempt));
continue;
Expand Down Expand Up @@ -813,6 +816,7 @@ export async function beginEvidenceRun(
* The pointer is compare-cleared only after the manifest is durable, so a
* concurrently replaced pointer is never removed.
*/
// eslint-disable-next-line complexity -- Legacy gate debt: pickforge/picklab#60
export async function finalizeActiveEvidenceRun(
projectDir: string,
sessionId: string,
Expand Down Expand Up @@ -1445,6 +1449,7 @@ async function commitTruncationSentinel(
*
* Returns true only for the process that actually appended the marker.
*/
// eslint-disable-next-line complexity -- Legacy gate debt: pickforge/picklab#60
async function writeTruncationMarkerOnce(
runDir: string,
handle: fs.promises.FileHandle,
Expand Down Expand Up @@ -1475,6 +1480,7 @@ async function writeTruncationMarkerOnce(
const ownedByCaller =
state.claim.ownerPid === ownerPid &&
state.claim.ownerStartTicks === ownerStartTicks;
// eslint-disable-next-line max-depth -- Legacy gate debt: pickforge/picklab#60
if (ownedByCaller && (await journalHasTruncationMarker(runDir))) {
await commitTruncationSentinel(sentinelPath, ownerPid, ownerStartTicks);
}
Expand Down Expand Up @@ -1683,6 +1689,7 @@ async function collectActiveRunIds(parent: string): Promise<Set<string>> {
* `finalizeActiveEvidenceRun` calls this automatically after every
* finalization.
*/
// eslint-disable-next-line complexity -- Legacy gate debt: pickforge/picklab#60
export async function pruneFinalizedEvidenceRuns(
projectDir: string,
opts: PruneEvidenceOptions = {},
Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/proc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,13 @@ export function runCommand(
args: readonly string[],
opts?: RunCommandOptions,
): Promise<RunCommandResult>;
// eslint-disable-next-line max-lines-per-function -- Legacy gate debt: pickforge/picklab#60
export function runCommand(
cmd: string,
args: readonly string[],
opts: RunCommandOptions = {},
): Promise<RunCommandResult> {
// eslint-disable-next-line max-lines-per-function -- Legacy gate debt: pickforge/picklab#60
return new Promise((resolve, reject) => {
const maxBytes = opts.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
const killGraceMs = opts.killGraceMs ?? DEFAULT_KILL_GRACE_MS;
Expand Down Expand Up @@ -491,6 +493,7 @@ function signalGroup(pid: number, signal: NodeJS.Signals): void {
* The leader must have been spawned as a process-group leader (e.g. `spawn`
* with `detached: true`), so its PID doubles as the group id.
*/
// eslint-disable-next-line complexity -- Legacy gate debt: pickforge/picklab#60
export async function stopProcessGroupVerified(
identity: ProcessIdentity,
opts: { timeoutMs?: number } = {},
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/redact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ function isEmbeddingBoundary(text: string, index: number): boolean {
* XML/JSON embedding: a quote that does not open a value ends the header so
* the surrounding document's delimiters survive.
*/
// eslint-disable-next-line complexity -- Legacy gate debt: pickforge/picklab#60
function redactCookiePairs(
text: string,
start: number,
Expand Down
Loading
Loading