vouchington-tooling 0.19.2 → 0.21.0

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 (36) hide show
  1. package/README.md +14 -9
  2. package/dist/cli/commands/gha-runtime-audit.mjs +6 -0
  3. package/dist/cli/commands/retrospective-facts.mjs +20 -7
  4. package/dist/cli/index.mjs +4 -1
  5. package/dist/cli/parse-gha-runtime-audit.d.mts +2 -0
  6. package/dist/cli/parse-gha-runtime-audit.mjs +33 -1
  7. package/dist/cli/parse.d.mts +7 -0
  8. package/dist/cli/parse.mjs +15 -0
  9. package/dist/cli/usage.d.mts +3 -1
  10. package/dist/cli/usage.mjs +44 -1
  11. package/dist/dependency-license-policy/audit-directory.d.mts +28 -0
  12. package/dist/dependency-license-policy/audit-directory.mjs +115 -0
  13. package/dist/dependency-license-policy/audit-signals.d.mts +12 -0
  14. package/dist/dependency-license-policy/audit-signals.mjs +44 -0
  15. package/dist/dependency-license-policy/audit-store.d.mts +12 -0
  16. package/dist/dependency-license-policy/audit-store.mjs +32 -0
  17. package/dist/dependency-license-policy/collect.d.mts +3 -1
  18. package/dist/dependency-license-policy/collect.mjs +16 -21
  19. package/dist/dependency-license-policy/execute-pnpm.d.mts +10 -0
  20. package/dist/dependency-license-policy/execute-pnpm.mjs +83 -0
  21. package/dist/dependency-license-policy/types.d.mts +11 -9
  22. package/dist/dependency-license-policy/workspace.d.mts +3 -1
  23. package/dist/dependency-license-policy/workspace.mjs +12 -5
  24. package/dist/gha-runtime-audit/audit.mjs +12 -4
  25. package/dist/gha-runtime-audit/family.d.mts +7 -0
  26. package/dist/gha-runtime-audit/family.mjs +79 -0
  27. package/dist/gha-runtime-audit/index.test-helpers.d.mts +5 -0
  28. package/dist/gha-runtime-audit/model.d.mts +11 -1
  29. package/dist/gha-runtime-audit/model.mjs +26 -0
  30. package/dist/gha-runtime-audit/scope.d.mts +2 -0
  31. package/dist/gha-runtime-audit/scope.mjs +16 -1
  32. package/dist/pnpm-install/support.mjs +1 -1
  33. package/package.json +1 -1
  34. package/scripts/allocate-browser-safe-ports.py +2 -583
  35. package/dist/cli/commands/allocate-browser-safe-ports.slot-fixtures.test-helpers.d.mts +0 -6
  36. package/dist/cli/commands/allocate-browser-safe-ports.slot-fixtures.test-helpers.mjs +0 -103
package/README.md CHANGED
@@ -326,19 +326,24 @@ policy out of this package.
326
326
  dependency declared by a non-fixture package manifest. Assert dependency membership or placement,
327
327
  or derive a configuration or documentation package spec from that manifest instead.
328
328
 
329
- `dependency-license-policy` keeps legal policy in the consumer. `collectPnpmLicenseReport` creates
330
- an isolated, script-free temporary workspace and store, expands pnpm's supported architectures to
331
- every `os`, `cpu`, and `libc` selector represented in the lockfile, drops every `engines` constraint
332
- from the audit copy of the lockfile, and validates the JSON report. Dropping `engines` keeps
333
- `pnpm fetch` from skipping optional packages that exclude the running Node.js; pnpm 12's `fetch`
334
- ignores `force`, so their licenses would otherwise report as Unknown.
329
+ `dependency-license-policy` keeps legal policy in the consumer. `collectPnpmLicenseReport` returns
330
+ a promise. It creates an isolated, script-free temporary workspace, expands pnpm's supported
331
+ architectures to every `os`, `cpu`, and `libc` selector represented in the lockfile, drops every
332
+ `engines` constraint from the audit copy of the lockfile, and validates the JSON report. Dropping
333
+ `engines` keeps `pnpm fetch` from skipping optional packages that exclude the running Node.js;
334
+ pnpm 12's `fetch` ignores `force`, so their licenses would otherwise report as Unknown.
335
+ Packages are fetched into a dedicated owner-only store under the pnpm cache
336
+ (`dependency-license-audit-store`) so a later audit reuses content-addressed packages instead of
337
+ downloading every platform again, without writing those packages into the developer store.
335
338
  Pass explicit denied SPDX IDs and prefixes, exact aliases, and justified allowlist scopes to
336
339
  `evaluatePnpmLicenseReport`. Unknown, malformed, and custom SPDX references fail closed. Allowlist
337
340
  scopes are either intentionally global or an exact package-name set; the library returns structured
338
341
  violations and does not format CI-provider diagnostics.
339
- When present, the repository `.npmrc` is copied into the owner-private temporary directory so pnpm
340
- can authenticate to the same registries; normal cleanup removes the copy, and the caller remains
341
- responsible for terminating the process normally rather than abandoning temporary audit state.
342
+ When present, the repository `.npmrc` is copied into the owner-private temporary workspace so pnpm
343
+ can authenticate to the same registries. The workspace is removed when the audit finishes, when the
344
+ process receives SIGINT, SIGTERM, or SIGHUP, and on the next audit if the previous process died
345
+ first, including SIGKILL. Each workspace records its owner's PID so a later audit can delete
346
+ directories whose owner is gone.
342
347
 
343
348
  `session-friction` is an opt-in capture and reporting library. Callers supply the session id,
344
349
  absolute log directory, host-independent observation, and journal loader; it does not inspect host
@@ -17,6 +17,12 @@ export async function runGhaRuntimeAudit(parsed, execute = defaultGhApiExecutor(
17
17
  repository,
18
18
  workflows: parsed.workflows,
19
19
  ...(parsed.branch === undefined ? {} : { branch: parsed.branch }),
20
+ ...(parsed.medianFloorSeconds === undefined
21
+ ? {}
22
+ : { medianFloorSeconds: parsed.medianFloorSeconds }),
23
+ ...(parsed.medianThresholdSeconds === undefined
24
+ ? {}
25
+ : { medianThresholdSeconds: parsed.medianThresholdSeconds }),
20
26
  });
21
27
  process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
22
28
  return 0;
@@ -6,24 +6,34 @@ export async function runRetrospectiveFactsCommand(args) {
6
6
  args,
7
7
  strict: true,
8
8
  options: {
9
- pr: { type: 'string' },
9
+ pr: { type: 'string', multiple: true },
10
10
  branch: { type: 'string' },
11
11
  'no-pr': { type: 'boolean' },
12
12
  repo: { type: 'string' },
13
13
  raw: { type: 'boolean' },
14
14
  },
15
15
  });
16
- const options = {
17
- ...(values.pr === undefined ? {} : { pr: values.pr }),
16
+ const prs = values.pr ?? [];
17
+ const shared = {
18
18
  ...(values.branch === undefined ? {} : { branch: values.branch }),
19
19
  ...(values['no-pr'] === undefined ? {} : { noPr: values['no-pr'] }),
20
20
  ...(values.repo === undefined ? {} : { repo: values.repo }),
21
21
  ...(values.raw ? { raw: true } : {}),
22
22
  };
23
- process.stdout.write(await runRetrospectiveFacts({
24
- ...options,
25
- onWarning: (message) => process.stderr.write(`${message}\n`),
26
- }));
23
+ const selectors = prs.length === 0 ? [undefined] : prs;
24
+ const blocks = [];
25
+ for (const pr of selectors) {
26
+ blocks.push(await runRetrospectiveFacts({
27
+ ...shared,
28
+ ...(pr === undefined ? {} : { pr }),
29
+ onWarning: (message) => process.stderr.write(`${message}\n`),
30
+ }));
31
+ }
32
+ if (blocks.length === 1) {
33
+ process.stdout.write(blocks[0]);
34
+ return 0;
35
+ }
36
+ process.stdout.write(`${joinFactBlocks(blocks)}\n`);
27
37
  return 0;
28
38
  }
29
39
  catch (error) {
@@ -31,3 +41,6 @@ export async function runRetrospectiveFactsCommand(args) {
31
41
  return 2;
32
42
  }
33
43
  }
44
+ function joinFactBlocks(blocks) {
45
+ return blocks.map((block) => block.replace(/\n+$/, '')).join('\n\n');
46
+ }
@@ -28,7 +28,7 @@ import { runAstGrepPackCommand } from './commands/ast-grep-pack.mjs';
28
28
  import { runGhaWorkspacePolicy } from './commands/gha-workspace-policy.mjs';
29
29
  import { parseCli } from './parse.mjs';
30
30
  import { packageScriptPath } from './script-path.mjs';
31
- import { printUsage } from './usage.mjs';
31
+ import { commandUsage, printUsage } from './usage.mjs';
32
32
  const SCRIPT_PATHS = {
33
33
  'gha-output': { command: 'bash', path: 'scripts/gha/write-github-multiline-output.sh' },
34
34
  'gha-needs-results': { command: 'bash', path: 'scripts/gha/check-needs-results.sh' },
@@ -79,6 +79,9 @@ export function runCli(argv = process.argv) {
79
79
  case 'help':
80
80
  printUsage();
81
81
  return 0;
82
+ case 'command-help':
83
+ process.stdout.write(commandUsage(parsed.command));
84
+ return 0;
82
85
  case 'version':
83
86
  process.stdout.write(`${readInstalledVersion()}\n`);
84
87
  return 0;
@@ -4,6 +4,8 @@ export type ParsedGhaRuntimeAudit = {
4
4
  repository?: string;
5
5
  branch?: string;
6
6
  workflows: RuntimeAuditWorkflowFilter[];
7
+ medianFloorSeconds?: number;
8
+ medianThresholdSeconds?: number;
7
9
  };
8
10
  export declare function parseGhaRuntimeAudit(args: readonly string[]): ParsedGhaRuntimeAudit | {
9
11
  kind: 'help';
@@ -2,6 +2,8 @@ import { parseWorkflowNameMatch, } from '../gha-runtime-audit/scope.mjs';
2
2
  export function parseGhaRuntimeAudit(args) {
3
3
  let repository;
4
4
  let branch;
5
+ let medianFloorSeconds;
6
+ let medianThresholdSeconds;
5
7
  const workflows = [];
6
8
  for (let index = 0; index < args.length; index += 1) {
7
9
  const flag = args[index];
@@ -10,7 +12,9 @@ export function parseGhaRuntimeAudit(args) {
10
12
  if (flag === '--repository' ||
11
13
  flag === '--pr-workflow' ||
12
14
  flag === '--push-workflow' ||
13
- flag === '--branch') {
15
+ flag === '--branch' ||
16
+ flag === '--median-threshold-floor' ||
17
+ flag === '--median-threshold-ceiling') {
14
18
  const value = args[index + 1];
15
19
  if (value === undefined)
16
20
  return { kind: 'error', message: `${flag} requires a value` };
@@ -19,6 +23,15 @@ export function parseGhaRuntimeAudit(args) {
19
23
  repository = value;
20
24
  else if (flag === '--branch')
21
25
  branch = value;
26
+ else if (flag === '--median-threshold-floor' || flag === '--median-threshold-ceiling') {
27
+ const seconds = parsePositiveSeconds(flag, value);
28
+ if (typeof seconds !== 'number')
29
+ return seconds;
30
+ if (flag === '--median-threshold-floor')
31
+ medianFloorSeconds = seconds;
32
+ else
33
+ medianThresholdSeconds = seconds;
34
+ }
22
35
  else {
23
36
  workflows.push({
24
37
  name: parseWorkflowNameMatch(value),
@@ -35,10 +48,29 @@ export function parseGhaRuntimeAudit(args) {
35
48
  message: 'gha-runtime-audit requires --pr-workflow or --push-workflow',
36
49
  };
37
50
  }
51
+ const ceiling = medianThresholdSeconds ?? 360;
52
+ if (medianFloorSeconds !== undefined && medianFloorSeconds >= ceiling) {
53
+ return {
54
+ kind: 'error',
55
+ message: '--median-threshold-floor must be below the median ceiling',
56
+ };
57
+ }
38
58
  return {
39
59
  kind: 'gha-runtime-audit',
40
60
  workflows,
41
61
  ...(repository === undefined ? {} : { repository }),
42
62
  ...(branch === undefined ? {} : { branch }),
63
+ ...(medianFloorSeconds === undefined ? {} : { medianFloorSeconds }),
64
+ ...(medianThresholdSeconds === undefined ? {} : { medianThresholdSeconds }),
43
65
  };
44
66
  }
67
+ function parsePositiveSeconds(flag, value) {
68
+ if (!/^[1-9]\d*$/.test(value)) {
69
+ return { kind: 'error', message: `${flag} requires a positive integer` };
70
+ }
71
+ const parsed = Number(value);
72
+ if (!Number.isSafeInteger(parsed)) {
73
+ return { kind: 'error', message: `${flag} requires a positive integer` };
74
+ }
75
+ return parsed;
76
+ }
@@ -2,6 +2,9 @@ import { type ParsedGhaArtifactsCleanup } from './parse-gha-artifacts-cleanup.mt
2
2
  import { type ParsedGhaRuntimeAudit } from './parse-gha-runtime-audit.mts';
3
3
  export type ParsedCli = {
4
4
  kind: 'help';
5
+ } | {
6
+ kind: 'command-help';
7
+ command: string;
5
8
  } | {
6
9
  kind: 'version';
7
10
  } | {
@@ -85,3 +88,7 @@ export type ParsedCli = {
85
88
  } | ParsedGhaRuntimeAudit | ParsedGhaArtifactsCleanup;
86
89
  export type ScriptCommand = 'gha-output' | 'gha-needs-results' | 'download-with-diagnostics' | 'download-optional-run-artifacts' | 'host-pressure-diagnostics' | 'allocate-browser-safe-ports' | 'diagnose-port-collision' | 'prepare-trivy-db' | 'check-cache-size' | 'make-shard-matrix' | 'load-runner-env' | 'clean-workspace' | 'install-github-release' | 'run-with-timeout' | 'lint-links' | 'materialize-pr-context' | 'wait-for-apt-locks' | 'install-playwright-chromium-arm64' | 'ghcr-package-retention' | 'harness-admission-lane' | 'harness-assert-gates';
87
90
  export declare function parseCli(argv: readonly string[]): ParsedCli;
91
+ export declare function parseCommandHelp(command: string | undefined, rest: readonly string[]): {
92
+ kind: 'command-help';
93
+ command: string;
94
+ } | undefined;
@@ -1,5 +1,6 @@
1
1
  import { parseGhaArtifactsCleanup, } from './parse-gha-artifacts-cleanup.mjs';
2
2
  import { parseGhaRuntimeAudit } from './parse-gha-runtime-audit.mjs';
3
+ import { commandNames } from './usage.mjs';
3
4
  import { parseAstGrepExamples, parseAstGrepPack, parseGhaWorkspacePolicy, parseGitleaksDirectoryScan, parseHttpOrigin, parseLinkSkill, parseRequireUpToDate, parseRunnerPortPolicy, } from './parse-options.mjs';
4
5
  const SCRIPT_COMMANDS = new Set([
5
6
  'gha-output',
@@ -31,6 +32,9 @@ export function parseCli(argv) {
31
32
  if (args[0] === '--version' || args[0] === '-v')
32
33
  return { kind: 'version' };
33
34
  const [command, ...rest] = args;
35
+ const help = parseCommandHelp(command, rest);
36
+ if (help)
37
+ return help;
34
38
  if (command === 'runner-port-policy')
35
39
  return parseRunnerPortPolicy(rest);
36
40
  if (command === 'with-host-lock')
@@ -82,3 +86,14 @@ export function parseCli(argv) {
82
86
  }
83
87
  return { kind: 'error', message: `unknown command: ${command}` };
84
88
  }
89
+ export function parseCommandHelp(command, rest) {
90
+ if (command === undefined || !commandNames().includes(command) || !helpBeforeSeparator(rest)) {
91
+ return undefined;
92
+ }
93
+ return { kind: 'command-help', command };
94
+ }
95
+ function helpBeforeSeparator(args) {
96
+ const separator = args.indexOf('--');
97
+ const options = separator === -1 ? args : args.slice(0, separator);
98
+ return options.includes('--help') || options.includes('-h');
99
+ }
@@ -1,2 +1,4 @@
1
- export declare const USAGE = "Usage: vouchington <command> [options]\n\nCommands:\n runner-port-policy Print or validate a runner port policy\n with-host-lock Run a command under a host-wide lock\n agent-harness-config Apply classifier-auto + sandbox keys to agent harnesses\n gha-runtime-audit Audit successful GitHub Actions job runtimes\n require-up-to-date Require HEAD to include a fetched remote branch\n gitleaks-directory-scan Scan a directory with Gitleaks\n ast-grep-examples Run AST-grep rule examples\n ast-grep-pack Print shipped unconditional AST-grep pack paths as JSON\n gha-workspace-policy Check GitHub Actions workspace safety policy\n gha-output Write a collision-safe multiline GITHUB_OUTPUT record\n gha-needs-results Fail if required GitHub Actions job results failed\n download-with-diagnostics Download a URL and report HTTP status on failure\n download-optional-run-artifacts Download optional artifacts from the current run\n host-pressure-diagnostics Print a bounded host memory/OOM/PSI snapshot\n allocate-browser-safe-ports Allocate Fetch-safe localhost ports\n diagnose-port-collision Capture bounded localhost port diagnostics\n prepare-trivy-db Download the Trivy vulnerability database\n gha-artifacts-cleanup Delete classified GitHub Actions artifacts\n http-origin Validate an optional HTTP(S) origin\n vitest-blob-manifest Stamp a vitest-blob-manifest:v1 identity file\n vitest-report-attempt Write or read a Vitest report-attempt marker\n prepare-vitest-reports Validate and select Vitest report JSON files\n pnpm-install Install a pnpm workspace with retry and release-age fail-fast\n check-cache-size Measure a path and decide whether to save a GHA cache\n make-shard-matrix Emit a [1..N] GitHub Actions shard matrix\n load-runner-env Overlay a runner env file onto GITHUB_ENV with injection guards\n clean-workspace Reset a persistent-runner workspace with a fork-PR trust gate\n install-github-release Download a checksum-verified GitHub Release binary\n run-with-timeout Run a command with GNU timeout or a Perl fallback\n lint-links Two-pass lychee: internal links fail, external warn\n materialize-pr-context Dump PR title/body/files/diff/comments and #N crawl\n wait-for-apt-locks Wait until apt/dpkg lock files are free\n install-playwright-chromium-arm64 Install Playwright Chromium from browsers.json\n ghcr-package-retention Delete old GHCR package versions past KEEP_MIN\n harness-admission-lane Compute a GITHUB_RUN_ID admission lane for fleet fan-out\n harness-assert-gates Fail if any named HARNESS_*_ENABLED gate is enabled\n nuget-central-version Validate a Directory.Packages.props PackageVersion delta\n swift-semantic-equal Compare Swift sources ignoring comments and whitespace\n post-review Post one COMMENT review from a staged payload file\n stage-review-payload Validate a review payload file into a staging directory\n retrospective-transcript Format facts from Claude-compatible, Codex, or Grok transcripts\n link-skill Link one packaged skill into an explicit consumer directory\n retrospective-facts Gather immutable facts for a retrospective\n agent-blackboard Probe and journal an Agent Blackboard deployment\n\nOptions:\n -h, --help Show this help\n -v, --version Print the package version\n\nrunner-port-policy\n (no args) Print the shipped policy as JSON\n --file <path> Validate and print a policy file\n --reserved <port> Print true if the port is reserved\n\nwith-host-lock\n --name <family>\n [--slots <n>]\n --timeout-seconds <n>\n [--command-timeout-seconds <n>]\n [--failure-diagnostics <absolute-script>]\n [--on-acquire-timeout fail|run-unlocked]\n -- <command> [args...]\n\nagent-harness-config dump\nagent-harness-config check|apply [--global] [--repo PATH]... [--harness claude|codex|grok|cursor]...\n [--home PATH]\n\ngha-runtime-audit\n [--repository owner/name] Default GITHUB_REPOSITORY\n [--branch main]\n --pr-workflow <name|/regex/> Repeatable\n --push-workflow <name|/regex/> Repeatable\n\nrequire-up-to-date --remote <name> --branch <name>\ngitleaks-directory-scan --config <path> [--directory <path>]\nast-grep-examples --rules <directory> --config <path>\nast-grep-pack\ngha-workspace-policy [--root <directory>] [--workflow-directory <directory>] [--action-directory <directory>]\n\ngha-output <name>\ngha-needs-results [label]\ndownload-with-diagnostics <url> <destination> [-- curl-args...]\ndownload-optional-run-artifacts (--name <name>... | --pattern <pattern>) --dir <directory>\nhost-pressure-diagnostics\nallocate-browser-safe-ports [count] [--policy path] [--forbidden-ports path]\ndiagnose-port-collision [--ports \"2200 2216\"] [--output-dir PATH]\nprepare-trivy-db\ngha-artifacts-cleanup run --run-id <id> [--keep-pattern glob] [--delete-pattern glob] [--patterns-file json]\ngha-artifacts-cleanup sweep --older-than-hours <n> [--keep-pattern glob] [--delete-pattern glob] [--patterns-file json]\nhttp-origin [--field NAME] [value]\nvitest-blob-manifest <suite> [reports-directory]\nvitest-report-attempt <write DIRECTORY SUITE|read ROOT>\nprepare-vitest-reports [primary-directory] [fallback-directory] [output-directory]\npnpm-install --runner-lifecycle persistent|ephemeral|ephemeral-full --install-scripts true|false\ncheck-cache-size <path> <max-bytes> <label>\nmake-shard-matrix <total>\nload-runner-env\nclean-workspace\ninstall-github-release --repo owner/name --version X --asset 'name-{platform}.tar.gz' --bin name [--tag-prefix PREFIX] [--expected-sha256 SHA256] [--no-checksum] [--checksums-asset NAME] [--version-flag FLAG] [--bin-dir DIR]\nrun-with-timeout <timeout-seconds> <kill-after-seconds> <command...>\nlint-links [--offline] [--config PATH] [--glob PATTERN] [files...]\nmaterialize-pr-context\nwait-for-apt-locks\ninstall-playwright-chromium-arm64 [name:archive...]\nghcr-package-retention <url-encoded-package>...\nharness-admission-lane <lanes>\nharness-assert-gates <gate>...\nnuget-central-version <trusted-props> <candidate-props> <metadata-json> <output-props>\nswift-semantic-equal <base> <head> <file.swift>\npost-review\nstage-review-payload optional|required <source> <destination>\nretrospective-transcript [--session-id ID] [--jsonl PATH] [--projects-dir PATH] [--codex-sessions-dir PATH] [--grok-sessions-dir PATH]\nlink-skill <name> --source-root <skills-dir> --target-root <consumer-skills-dir> Link a packaged or repository-local skill\nretrospective-facts (--pr NUMBER | --branch NAME | --no-pr) [--repo OWNER/NAME] [--raw]\nagent-blackboard probe\nagent-blackboard journal append --session-id UUID --agent NAME --version VERSION --file PATH --repository OWNER/NAME [--repository OWNER/NAME ...] [--parent-session-id UUID] [--timestamp ISO8601]\nagent-blackboard journal entries --session-id UUID\nagent-blackboard snapshot partition --snapshot PATH --checksum SHA256 --counts '{\"sessions\":N,\"entries\":N,\"records\":N,\"bytes\":N}'\nagent-blackboard snapshot cleanup [--snapshot PATH] [--partition-directory PATH --receipt JSON]\n";
1
+ export declare const USAGE = "Usage: vouchington <command> [options]\n\nCommands:\n runner-port-policy Print or validate a runner port policy\n with-host-lock Run a command under a host-wide lock\n agent-harness-config Apply classifier-auto + sandbox keys to agent harnesses\n gha-runtime-audit Audit successful GitHub Actions job runtimes\n require-up-to-date Require HEAD to include a fetched remote branch\n gitleaks-directory-scan Scan a directory with Gitleaks\n ast-grep-examples Run AST-grep rule examples\n ast-grep-pack Print shipped unconditional AST-grep pack paths as JSON\n gha-workspace-policy Check GitHub Actions workspace safety policy\n gha-output Write a collision-safe multiline GITHUB_OUTPUT record\n gha-needs-results Fail if required GitHub Actions job results failed\n download-with-diagnostics Download a URL and report HTTP status on failure\n download-optional-run-artifacts Download optional artifacts from the current run\n host-pressure-diagnostics Print a bounded host memory/OOM/PSI snapshot\n allocate-browser-safe-ports Allocate Fetch-safe localhost ports\n diagnose-port-collision Capture bounded localhost port diagnostics\n prepare-trivy-db Download the Trivy vulnerability database\n gha-artifacts-cleanup Delete classified GitHub Actions artifacts\n http-origin Validate an optional HTTP(S) origin\n vitest-blob-manifest Stamp a vitest-blob-manifest:v1 identity file\n vitest-report-attempt Write or read a Vitest report-attempt marker\n prepare-vitest-reports Validate and select Vitest report JSON files\n pnpm-install Install a pnpm workspace with retry and release-age fail-fast\n check-cache-size Measure a path and decide whether to save a GHA cache\n make-shard-matrix Emit a [1..N] GitHub Actions shard matrix\n load-runner-env Overlay a runner env file onto GITHUB_ENV with injection guards\n clean-workspace Reset a persistent-runner workspace with a fork-PR trust gate\n install-github-release Download a checksum-verified GitHub Release binary\n run-with-timeout Run a command with GNU timeout or a Perl fallback\n lint-links Two-pass lychee: internal links fail, external warn\n materialize-pr-context Dump PR title/body/files/diff/comments and #N crawl\n wait-for-apt-locks Wait until apt/dpkg lock files are free\n install-playwright-chromium-arm64 Install Playwright Chromium from browsers.json\n ghcr-package-retention Delete old GHCR package versions past KEEP_MIN\n harness-admission-lane Compute a GITHUB_RUN_ID admission lane for fleet fan-out\n harness-assert-gates Fail if any named HARNESS_*_ENABLED gate is enabled\n nuget-central-version Validate a Directory.Packages.props PackageVersion delta\n swift-semantic-equal Compare Swift sources ignoring comments and whitespace\n post-review Post one COMMENT review from a staged payload file\n stage-review-payload Validate a review payload file into a staging directory\n retrospective-transcript Format facts from Claude-compatible, Codex, or Grok transcripts\n link-skill Link one packaged skill into an explicit consumer directory\n retrospective-facts Gather immutable facts for a retrospective\n agent-blackboard Probe and journal an Agent Blackboard deployment\n\nOptions:\n -h, --help Show this help\n -v, --version Print the package version\n\nrunner-port-policy\n (no args) Print the shipped policy as JSON\n --file <path> Validate and print a policy file\n --reserved <port> Print true if the port is reserved\n\nwith-host-lock\n --name <family>\n [--slots <n>]\n --timeout-seconds <n>\n [--command-timeout-seconds <n>]\n [--failure-diagnostics <absolute-script>]\n [--on-acquire-timeout fail|run-unlocked]\n -- <command> [args...]\n\nagent-harness-config dump\nagent-harness-config check|apply [--global] [--repo PATH]... [--harness claude|codex|grok|cursor]...\n [--home PATH]\n\ngha-runtime-audit\n [--repository owner/name] Default GITHUB_REPOSITORY\n [--branch main]\n [--median-threshold-floor <seconds>] Off unless set. Must be below the ceiling\n [--median-threshold-ceiling <seconds>] Default 360. Flags a five-sample median above this\n --pr-workflow <name|/regex/> Repeatable\n --push-workflow <name|/regex/> Repeatable\n\nrequire-up-to-date --remote <name> --branch <name>\ngitleaks-directory-scan --config <path> [--directory <path>]\nast-grep-examples --rules <directory> --config <path>\nast-grep-pack\ngha-workspace-policy [--root <directory>] [--workflow-directory <directory>] [--action-directory <directory>]\n\ngha-output <name>\ngha-needs-results [label]\ndownload-with-diagnostics <url> <destination> [-- curl-args...]\ndownload-optional-run-artifacts (--name <name>... | --pattern <pattern>) --dir <directory>\nhost-pressure-diagnostics\nallocate-browser-safe-ports <count> [--policy path] [--forbidden-ports path]\ndiagnose-port-collision [--ports \"2200 2216\"] [--output-dir PATH]\nprepare-trivy-db\ngha-artifacts-cleanup run --run-id <id> [--keep-pattern glob] [--delete-pattern glob] [--patterns-file json]\ngha-artifacts-cleanup sweep --older-than-hours <n> [--keep-pattern glob] [--delete-pattern glob] [--patterns-file json]\nhttp-origin [--field NAME] [value]\nvitest-blob-manifest <suite> [reports-directory]\nvitest-report-attempt <write DIRECTORY SUITE|read ROOT>\nprepare-vitest-reports [primary-directory] [fallback-directory] [output-directory]\npnpm-install --runner-lifecycle persistent|ephemeral|ephemeral-full --install-scripts true|false\ncheck-cache-size <path> <max-bytes> <label>\nmake-shard-matrix <total>\nload-runner-env\nclean-workspace\ninstall-github-release --repo owner/name --version X --asset 'name-{platform}.tar.gz' --bin name [--tag-prefix PREFIX] [--expected-sha256 SHA256] [--no-checksum] [--checksums-asset NAME] [--version-flag FLAG] [--bin-dir DIR]\nrun-with-timeout <timeout-seconds> <kill-after-seconds> <command...>\nlint-links [--offline] [--config PATH] [--glob PATTERN] [files...]\nmaterialize-pr-context\nwait-for-apt-locks\ninstall-playwright-chromium-arm64 [name:archive...]\nghcr-package-retention <url-encoded-package>...\nharness-admission-lane <lanes>\nharness-assert-gates <gate>...\nnuget-central-version <trusted-props> <candidate-props> <metadata-json> <output-props>\nswift-semantic-equal <base> <head> <file.swift>\npost-review\nstage-review-payload optional|required <source> <destination>\nretrospective-transcript [--session-id ID] [--jsonl PATH] [--projects-dir PATH] [--codex-sessions-dir PATH] [--grok-sessions-dir PATH]\nlink-skill <name> --source-root <skills-dir> --target-root <consumer-skills-dir> Link a packaged or repository-local skill\nretrospective-facts (--pr NUMBER | --branch NAME | --no-pr) [--repo OWNER/NAME] [--raw]\nagent-blackboard probe\nagent-blackboard journal append --session-id UUID --agent NAME --version VERSION --file PATH --repository OWNER/NAME [--repository OWNER/NAME ...] [--parent-session-id UUID] [--timestamp ISO8601]\nagent-blackboard journal entries --session-id UUID\nagent-blackboard snapshot partition --snapshot PATH --checksum SHA256 --counts '{\"sessions\":N,\"entries\":N,\"records\":N,\"bytes\":N}'\nagent-blackboard snapshot cleanup [--snapshot PATH] [--partition-directory PATH --receipt JSON]\n";
2
2
  export declare function printUsage(stream?: NodeJS.WritableStream): void;
3
+ export declare function commandNames(usage?: string): string[];
4
+ export declare function commandUsage(command: string, usage?: string): string;
@@ -71,6 +71,8 @@ agent-harness-config check|apply [--global] [--repo PATH]... [--harness claude|c
71
71
  gha-runtime-audit
72
72
  [--repository owner/name] Default GITHUB_REPOSITORY
73
73
  [--branch main]
74
+ [--median-threshold-floor <seconds>] Off unless set. Must be below the ceiling
75
+ [--median-threshold-ceiling <seconds>] Default 360. Flags a five-sample median above this
74
76
  --pr-workflow <name|/regex/> Repeatable
75
77
  --push-workflow <name|/regex/> Repeatable
76
78
 
@@ -85,7 +87,7 @@ gha-needs-results [label]
85
87
  download-with-diagnostics <url> <destination> [-- curl-args...]
86
88
  download-optional-run-artifacts (--name <name>... | --pattern <pattern>) --dir <directory>
87
89
  host-pressure-diagnostics
88
- allocate-browser-safe-ports [count] [--policy path] [--forbidden-ports path]
90
+ allocate-browser-safe-ports <count> [--policy path] [--forbidden-ports path]
89
91
  diagnose-port-collision [--ports "2200 2216"] [--output-dir PATH]
90
92
  prepare-trivy-db
91
93
  gha-artifacts-cleanup run --run-id <id> [--keep-pattern glob] [--delete-pattern glob] [--patterns-file json]
@@ -124,3 +126,44 @@ agent-blackboard snapshot cleanup [--snapshot PATH] [--partition-directory PATH
124
126
  export function printUsage(stream = process.stdout) {
125
127
  stream.write(USAGE);
126
128
  }
129
+ export function commandNames(usage = USAGE) {
130
+ const names = [];
131
+ let inCommands = false;
132
+ for (const line of usage.split('\n')) {
133
+ if (line === 'Commands:') {
134
+ inCommands = true;
135
+ continue;
136
+ }
137
+ if (!inCommands)
138
+ continue;
139
+ if (line === '')
140
+ break;
141
+ const name = line.trim().split(/\s+/)[0];
142
+ if (name)
143
+ names.push(name);
144
+ }
145
+ return names;
146
+ }
147
+ export function commandUsage(command, usage = USAGE) {
148
+ const names = new Set(commandNames(usage));
149
+ if (!names.has(command))
150
+ throw new Error(`unknown command: ${command}`);
151
+ const lines = usage.split('\n');
152
+ const detailStart = lines.indexOf('Options:');
153
+ const selected = [];
154
+ let capturing = false;
155
+ for (const line of lines.slice(detailStart + 1)) {
156
+ if (line === '') {
157
+ if (capturing)
158
+ break;
159
+ continue;
160
+ }
161
+ if (!line.startsWith(' '))
162
+ capturing = line.trim().split(/\s+/)[0] === command;
163
+ if (capturing)
164
+ selected.push(line);
165
+ }
166
+ if (selected.length === 0)
167
+ throw new Error(`no usage for ${command}`);
168
+ return `${selected.join('\n')}\n`;
169
+ }
@@ -0,0 +1,28 @@
1
+ export declare const LICENSE_AUDIT_DIRECTORY_PREFIX = "dependency-license-audit-";
2
+ export interface LicenseAuditReclaimOptions {
3
+ readonly graceMs?: number;
4
+ readonly isOwnerAlive?: (pid: number, directoryMtimeMs: number) => boolean;
5
+ readonly now?: number;
6
+ }
7
+ export interface LicenseAuditWorkspaceOptions extends LicenseAuditReclaimOptions {
8
+ readonly directory?: string;
9
+ readonly pid?: number;
10
+ }
11
+ interface OwnerAliveDependencies {
12
+ readonly isProcessAlive?: (pid: number) => boolean;
13
+ readonly readProcessStartMs?: (pid: number) => number | undefined;
14
+ }
15
+ /** Parses `ps -o lstart=` output. Exported for tests of unparseable dates. */
16
+ export declare function parseProcessStart(stdout: string): number | undefined;
17
+ export declare function readProcessStartMs(pid: number): number | undefined;
18
+ export declare function isProcessAlive(pid: number): boolean;
19
+ /**
20
+ * A directory still belongs to its creator when that PID is alive and the process started
21
+ * before the directory. A recycled PID belongs to a newer process and is not the owner.
22
+ */
23
+ export declare function isAuditOwnerAlive(pid: number, directoryMtimeMs: number, dependencies?: OwnerAliveDependencies): boolean;
24
+ export declare function writeAuditPid(directory: string, pid?: number): void;
25
+ export declare function removeAuditDirectory(directory: string): void;
26
+ /** Deletes leftover audit directories whose owner process is gone, including after SIGKILL. */
27
+ export declare function reclaimStaleLicenseAuditDirectories(directory: string, options?: LicenseAuditReclaimOptions): void;
28
+ export {};
@@ -0,0 +1,115 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { chmodSync, lstatSync, readdirSync, readFileSync, rmSync, unlinkSync, writeFileSync, } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ export const LICENSE_AUDIT_DIRECTORY_PREFIX = 'dependency-license-audit-';
5
+ const LICENSE_AUDIT_PID_FILE = 'audit.pid';
6
+ const STALE_AUDIT_GRACE_MS = 60_000;
7
+ const OWNER_START_SLACK_MS = 1_000;
8
+ const MAX_PID = 2_147_483_647;
9
+ function hasCode(error, code) {
10
+ return Boolean(error && typeof error === 'object' && 'code' in error && error.code === code);
11
+ }
12
+ /** Parses `ps -o lstart=` output. Exported for tests of unparseable dates. */
13
+ export function parseProcessStart(stdout) {
14
+ const parsed = Date.parse(stdout.trim());
15
+ if (Number.isNaN(parsed))
16
+ return undefined;
17
+ return parsed;
18
+ }
19
+ export function readProcessStartMs(pid) {
20
+ const result = spawnSync('ps', ['-p', String(pid), '-o', 'lstart='], { encoding: 'utf8' });
21
+ if (result.status !== 0)
22
+ return undefined;
23
+ return parseProcessStart(result.stdout);
24
+ }
25
+ export function isProcessAlive(pid) {
26
+ if (!Number.isSafeInteger(pid) || pid <= 0 || pid > MAX_PID)
27
+ return false;
28
+ try {
29
+ process.kill(pid, 0);
30
+ return true;
31
+ }
32
+ catch (error) {
33
+ return !hasCode(error, 'ESRCH');
34
+ }
35
+ }
36
+ /**
37
+ * A directory still belongs to its creator when that PID is alive and the process started
38
+ * before the directory. A recycled PID belongs to a newer process and is not the owner.
39
+ */
40
+ export function isAuditOwnerAlive(pid, directoryMtimeMs, dependencies = {}) {
41
+ if (!(dependencies.isProcessAlive ?? isProcessAlive)(pid))
42
+ return false;
43
+ const started = (dependencies.readProcessStartMs ?? readProcessStartMs)(pid);
44
+ if (started === undefined)
45
+ return true;
46
+ return started <= directoryMtimeMs + OWNER_START_SLACK_MS;
47
+ }
48
+ export function writeAuditPid(directory, pid = process.pid) {
49
+ const path = join(directory, LICENSE_AUDIT_PID_FILE);
50
+ writeFileSync(path, `${String(pid)}\n`, { encoding: 'utf8', mode: 0o600 });
51
+ chmodSync(path, 0o600);
52
+ }
53
+ function readAuditPid(directory) {
54
+ try {
55
+ const text = readFileSync(join(directory, LICENSE_AUDIT_PID_FILE), 'utf8').trim();
56
+ if (!/^[1-9]\d*$/.test(text))
57
+ return undefined;
58
+ const pid = Number.parseInt(text, 10);
59
+ return Number.isSafeInteger(pid) && pid <= MAX_PID ? pid : undefined;
60
+ }
61
+ catch (error) {
62
+ if (hasCode(error, 'ENOENT'))
63
+ return undefined;
64
+ throw error;
65
+ }
66
+ }
67
+ export function removeAuditDirectory(directory) {
68
+ const stat = lstatSync(directory, { throwIfNoEntry: false });
69
+ if (!stat)
70
+ return;
71
+ if (stat.isSymbolicLink()) {
72
+ unlinkSync(directory);
73
+ return;
74
+ }
75
+ if (!stat.isDirectory())
76
+ return;
77
+ rmSync(directory, { force: true, recursive: true });
78
+ }
79
+ function reclaimNames(directory) {
80
+ try {
81
+ return readdirSync(directory, { encoding: 'utf8' });
82
+ }
83
+ catch (error) {
84
+ if (hasCode(error, 'ENOENT'))
85
+ return [];
86
+ throw error;
87
+ }
88
+ }
89
+ function shouldRemoveAuditDirectory(directory, mtimeMs, now, graceMs, isOwnerAlive) {
90
+ const pid = readAuditPid(directory);
91
+ if (pid === undefined)
92
+ return now - mtimeMs >= graceMs;
93
+ return !isOwnerAlive(pid, mtimeMs);
94
+ }
95
+ /** Deletes leftover audit directories whose owner process is gone, including after SIGKILL. */
96
+ export function reclaimStaleLicenseAuditDirectories(directory, options = {}) {
97
+ const now = options.now ?? Date.now();
98
+ const graceMs = options.graceMs ?? STALE_AUDIT_GRACE_MS;
99
+ const isOwnerAlive = options.isOwnerAlive ?? isAuditOwnerAlive;
100
+ for (const name of reclaimNames(directory)) {
101
+ if (!name.startsWith(LICENSE_AUDIT_DIRECTORY_PREFIX))
102
+ continue;
103
+ const path = join(directory, name);
104
+ const stat = lstatSync(path);
105
+ if (stat.isSymbolicLink()) {
106
+ unlinkSync(path);
107
+ continue;
108
+ }
109
+ if (!stat.isDirectory())
110
+ continue;
111
+ if (shouldRemoveAuditDirectory(path, stat.mtimeMs, now, graceMs, isOwnerAlive)) {
112
+ rmSync(path, { force: true, recursive: true });
113
+ }
114
+ }
115
+ }
@@ -0,0 +1,12 @@
1
+ type SignalListener = () => void;
2
+ export interface AuditSignalDependencies {
3
+ readonly raiseSignal?: (signal: NodeJS.Signals) => void;
4
+ readonly subscribe?: (signal: NodeJS.Signals, listener: SignalListener) => void;
5
+ readonly unsubscribe?: (signal: NodeJS.Signals, listener: SignalListener) => void;
6
+ }
7
+ /**
8
+ * Runs an audit while SIGINT, SIGTERM, and SIGHUP abort it, remove its workspace, and are raised
9
+ * again so the process still exits from that signal.
10
+ */
11
+ export declare function withAuditSignalCleanup<T>(cleanup: () => void, run: (signal: AbortSignal) => Promise<T>, dependencies?: AuditSignalDependencies): Promise<T>;
12
+ export {};
@@ -0,0 +1,44 @@
1
+ const AUDIT_SIGNALS = ['SIGINT', 'SIGTERM', 'SIGHUP'];
2
+ /**
3
+ * Runs an audit while SIGINT, SIGTERM, and SIGHUP abort it, remove its workspace, and are raised
4
+ * again so the process still exits from that signal.
5
+ */
6
+ export async function withAuditSignalCleanup(cleanup, run, dependencies = {}) {
7
+ const controller = new AbortController();
8
+ let raised;
9
+ const handlers = AUDIT_SIGNALS.map((signal) => {
10
+ const listener = () => {
11
+ if (raised !== undefined)
12
+ return;
13
+ raised = signal;
14
+ controller.abort(signal);
15
+ };
16
+ if (dependencies.subscribe)
17
+ dependencies.subscribe(signal, listener);
18
+ else
19
+ process.on(signal, listener);
20
+ return { listener, signal };
21
+ });
22
+ try {
23
+ return await run(controller.signal);
24
+ }
25
+ finally {
26
+ for (const { listener, signal } of handlers) {
27
+ if (dependencies.unsubscribe)
28
+ dependencies.unsubscribe(signal, listener);
29
+ else
30
+ process.removeListener(signal, listener);
31
+ }
32
+ try {
33
+ cleanup();
34
+ }
35
+ finally {
36
+ if (raised !== undefined) {
37
+ if (dependencies.raiseSignal)
38
+ dependencies.raiseSignal(raised);
39
+ else
40
+ process.kill(process.pid, raised);
41
+ }
42
+ }
43
+ }
44
+ }
@@ -0,0 +1,12 @@
1
+ export interface DirectoryIdentity {
2
+ readonly isDirectory: () => boolean;
3
+ readonly isSymbolicLink: () => boolean;
4
+ readonly mode: number;
5
+ readonly uid: number;
6
+ }
7
+ /** pnpm's cache directory. The audit store stays beside it so it is not the developer store. */
8
+ export declare function pnpmCacheDirectory(env?: NodeJS.ProcessEnv, home?: string, hostPlatform?: NodeJS.Platform): string;
9
+ export declare function licenseAuditStoreDirectory(env?: NodeJS.ProcessEnv, home?: string, hostPlatform?: NodeJS.Platform): string;
10
+ export declare function assertOwnedDirectory(stat: DirectoryIdentity, uid: number | undefined, directory: string): void;
11
+ /** Creates the dedicated content-addressed store used by repeat license audits. */
12
+ export declare function ensurePrivateLicenseAuditStore(directory: string): void;
@@ -0,0 +1,32 @@
1
+ import { chmodSync, lstatSync, mkdirSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ const STORE_DIRECTORY_NAME = 'dependency-license-audit-store';
5
+ /** pnpm's cache directory. The audit store stays beside it so it is not the developer store. */
6
+ export function pnpmCacheDirectory(env = process.env, home = homedir(), hostPlatform = process.platform) {
7
+ if (hostPlatform === 'darwin')
8
+ return join(home, 'Library', 'Caches', 'pnpm');
9
+ if (hostPlatform === 'win32') {
10
+ return join(env.LOCALAPPDATA ?? join(home, 'AppData', 'Local'), 'pnpm-cache');
11
+ }
12
+ return join(env.XDG_CACHE_HOME ?? join(home, '.cache'), 'pnpm');
13
+ }
14
+ export function licenseAuditStoreDirectory(env = process.env, home = homedir(), hostPlatform = process.platform) {
15
+ return join(pnpmCacheDirectory(env, home, hostPlatform), STORE_DIRECTORY_NAME);
16
+ }
17
+ export function assertOwnedDirectory(stat, uid, directory) {
18
+ if (stat.isSymbolicLink() || !stat.isDirectory()) {
19
+ throw new Error(`dependency license audit store is not a real directory: ${directory}`);
20
+ }
21
+ if (uid !== undefined && stat.uid !== uid) {
22
+ throw new Error(`dependency license audit store is not owned by the current user: ${directory}`);
23
+ }
24
+ }
25
+ /** Creates the dedicated content-addressed store used by repeat license audits. */
26
+ export function ensurePrivateLicenseAuditStore(directory) {
27
+ mkdirSync(directory, { recursive: true, mode: 0o700 });
28
+ const stat = lstatSync(directory);
29
+ assertOwnedDirectory(stat, process.getuid?.(), directory);
30
+ if ((stat.mode & 0o077) !== 0)
31
+ chmodSync(directory, 0o700);
32
+ }
@@ -1,9 +1,11 @@
1
1
  import type { PnpmExecutor, PnpmLicenseReport } from './types.mts';
2
2
  import { type PnpmLicenseAuditWorkspace } from './workspace.mts';
3
3
  export interface CollectPnpmLicenseReportOptions {
4
+ readonly ensureStore?: (directory: string) => void;
4
5
  readonly execute?: PnpmExecutor;
5
6
  readonly prepareWorkspace?: (repoRoot: string, lockfileSource: string, workspaceSource: string) => PnpmLicenseAuditWorkspace;
6
7
  readonly readFile?: (path: string, encoding: 'utf8') => string;
8
+ readonly storeDir?: string;
7
9
  }
8
10
  /** Collects licenses for every platform represented in a pnpm lockfile. */
9
- export declare function collectPnpmLicenseReport(repoRoot: string, options?: CollectPnpmLicenseReportOptions): PnpmLicenseReport;
11
+ export declare function collectPnpmLicenseReport(repoRoot: string, options?: CollectPnpmLicenseReportOptions): Promise<PnpmLicenseReport>;