vouchington-tooling 0.1.8 → 0.2.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 (64) hide show
  1. package/README.md +22 -5
  2. package/dist/cli/commands/link-skill.d.mts +5 -0
  3. package/dist/cli/commands/link-skill.mjs +6 -0
  4. package/dist/cli/index.mjs +3 -0
  5. package/dist/cli/parse.d.mts +5 -0
  6. package/dist/cli/parse.mjs +25 -0
  7. package/dist/cli/usage.d.mts +1 -1
  8. package/dist/cli/usage.mjs +2 -0
  9. package/dist/gha-post-review/github.d.mts +2 -0
  10. package/dist/gha-post-review/github.mjs +47 -5
  11. package/dist/gha-post-review/post.mjs +6 -1
  12. package/dist/index.d.mts +2 -0
  13. package/dist/index.mjs +1 -0
  14. package/dist/pnpm-install/index.d.mts +2 -1
  15. package/dist/pnpm-install/index.mjs +2 -1
  16. package/dist/pnpm-install/install-operations.d.mts +4 -0
  17. package/dist/pnpm-install/install-operations.mjs +38 -0
  18. package/dist/pnpm-install/metadata-legacy.d.mts +4 -0
  19. package/dist/pnpm-install/metadata-legacy.mjs +81 -0
  20. package/dist/pnpm-install/metadata.d.mts +16 -3
  21. package/dist/pnpm-install/metadata.mjs +103 -36
  22. package/dist/pnpm-install/pending-builds.d.mts +17 -0
  23. package/dist/pnpm-install/pending-builds.mjs +87 -0
  24. package/dist/pnpm-install/pnpm-install-fake-pnpm.test-helpers.d.mts +1 -0
  25. package/dist/pnpm-install/pnpm-install-fake-pnpm.test-helpers.mjs +52 -0
  26. package/dist/pnpm-install/pnpm-install-fixture.test-helpers.mjs +6 -40
  27. package/dist/pnpm-install/runner.mjs +72 -56
  28. package/dist/pnpm-install/transition.d.mts +23 -0
  29. package/dist/pnpm-install/transition.mjs +36 -0
  30. package/dist/skill-discovery/index.d.mts +12 -0
  31. package/dist/skill-discovery/index.mjs +64 -0
  32. package/dist/skill-discovery/manifest.d.mts +15 -0
  33. package/dist/skill-discovery/manifest.mjs +85 -0
  34. package/dist/skill-discovery/target-directory.d.mts +10 -0
  35. package/dist/skill-discovery/target-directory.mjs +126 -0
  36. package/package.json +6 -1
  37. package/scripts/build.mjs +16 -4
  38. package/skills/agent-workflow/SKILL.md +5 -0
  39. package/skills/agent-workflow/references/evidence-sweep.md +8 -0
  40. package/skills/agent-workflow/references/implementation-and-review.md +9 -0
  41. package/skills/agent-workflow/references/implementation.md +8 -0
  42. package/skills/agent-workflow/references/review.md +7 -0
  43. package/skills/backend-vitest-test-authoring/SKILL.md +15 -0
  44. package/skills/backend-vitest-test-authoring/references/integration-boundaries.md +10 -0
  45. package/skills/dotnet-test-authoring/SKILL.md +16 -0
  46. package/skills/manifest.json +158 -0
  47. package/skills/nextjs-vitest-test-authoring/SKILL.md +15 -0
  48. package/skills/nextjs-vitest-test-authoring/references/framework-boundaries.md +9 -0
  49. package/skills/planning/SKILL.md +3 -0
  50. package/skills/planning/references/impact-discovery.md +9 -0
  51. package/skills/playwright-authoring/SKILL.md +15 -0
  52. package/skills/playwright-authoring/references/browser-reliability.md +9 -0
  53. package/skills/postgres-node-performance-tuning/SKILL.md +15 -0
  54. package/skills/postgres-node-performance-tuning/references/performance-patterns.md +14 -0
  55. package/skills/postgres-partitioning-uuid-v7/SKILL.md +16 -0
  56. package/skills/postgres-partitioning-uuid-v7/references/partition-lifecycle.md +14 -0
  57. package/skills/storybook-authoring/SKILL.md +15 -0
  58. package/skills/storybook-authoring/references/component-coverage.md +9 -0
  59. package/skills/swift-test-authoring/SKILL.md +15 -0
  60. package/skills/swift-test-authoring/references/network-test-doubles.md +9 -0
  61. package/skills/test-authoring/SKILL.md +17 -0
  62. package/skills/test-authoring/references/core-practice.md +10 -0
  63. package/skills/vitest-test-authoring/SKILL.md +15 -0
  64. package/skills/vitest-test-authoring/references/mock-boundaries.md +9 -0
package/README.md CHANGED
@@ -47,6 +47,15 @@ vouchington post-review
47
47
  vouchington stage-review-payload optional|required <source> <destination>
48
48
  ```
49
49
 
50
+ For persistent `pnpm-install`, v4 metadata tracks structural inputs separately from the
51
+ `--install-scripts` policy. A warm scripts-enabled tree can therefore toggle
52
+ `true → false → true` without forced reconciliation; a tree first installed with scripts disabled
53
+ uses one script-suppressed verification install followed by `pnpm rebuild --pending --recursive`.
54
+ When only newly pending dependency package IDs remain, it instead rebuilds those exact IDs without
55
+ rerunning first-party workspace hooks.
56
+ The command emits a structured non-secret provenance diagnostic identifying changed structural
57
+ categories, the last script policy, script capability, and native-binary health.
58
+
50
59
  `download-optional-run-artifacts` uses the current Actions run and host. Pattern mode discovers
51
60
  non-expired artifacts across the run, keeps the first result for each name (matching `gh run
52
61
  download`), and extracts each selected name into its own directory. Ordinary absence is reported as
@@ -190,8 +199,16 @@ process-group probe and bounded-drain semantics outside a browser session.
190
199
 
191
200
  ## Workflow skills outside plugins
192
201
 
193
- The package ships the canonical Vouchington workflow skill tree at
194
- `skills/<skill>/SKILL.md`. This stable installed path supports agents that do not
195
- load Claude or Codex plugins. The Claude and Codex plugin manifests continue to reference the same
196
- canonical source tree under `plugins/vouchington-workflow/skills`; package build materializes that tree
197
- without hand-copying skill content.
202
+ The package ships a flat union of canonical workflow, testing, and database skills at
203
+ `skills/<skill>/SKILL.md`. This stable installed path supports agents that do not load Claude or
204
+ Codex plugins. The package build materializes plugin source trees without hand-copying skill content
205
+ and writes sorted schema-v1 provenance to `skills/manifest.json`.
206
+ Each manifest entry may declare its ordered `prerequisites`; ordinary Markdown links remain
207
+ cross-references and never cause additional skills to be linked.
208
+
209
+ Use `readSkillManifest(skillsRoot)` to discover installed skills or
210
+ `linkSkill({ name, sourceRoot, targetRoot })` to link one into an explicit consumer directory. The
211
+ CLI equivalent is `vouchington link-skill <name> --source-root <skills-dir> --target-root <dir>`.
212
+ It rejects unknown names, paths outside either root, and existing non-matching destinations.
213
+ `targetRoot` must already exist as a physical directory path: symlinked target roots or ancestors are
214
+ rejected.
@@ -0,0 +1,5 @@
1
+ export declare function runLinkSkill(options: {
2
+ name: string;
3
+ sourceRoot: string;
4
+ targetRoot: string;
5
+ }): Promise<number>;
@@ -0,0 +1,6 @@
1
+ import { linkSkill } from '../../skill-discovery/index.mjs';
2
+ export async function runLinkSkill(options) {
3
+ const result = await linkSkill(options);
4
+ process.stdout.write(`${result.created ? 'linked' : 'already-linked'} ${result.path}\n`);
5
+ return 0;
6
+ }
@@ -15,6 +15,7 @@ import { runStageReviewPayloadCommand } from './commands/stage-review-payload.mj
15
15
  import { runSwiftSemanticEqualCommand } from './commands/swift-semantic-equal.mjs';
16
16
  import { runVitestBlobManifestCommand } from './commands/vitest-blob-manifest.mjs';
17
17
  import { runRetrospectiveTranscriptCommand } from './commands/retrospective-transcript.mjs';
18
+ import { runLinkSkill } from './commands/link-skill.mjs';
18
19
  import { runWithHostLock } from './commands/with-host-lock.mjs';
19
20
  import { parseCli } from './parse.mjs';
20
21
  import { packageScriptPath } from './script-path.mjs';
@@ -99,6 +100,8 @@ export function runCli(argv = process.argv) {
99
100
  return runGhaArtifactsCleanup(parsed);
100
101
  case 'retrospective-transcript':
101
102
  return runRetrospectiveTranscriptCommand(parsed.args);
103
+ case 'link-skill':
104
+ return runLinkSkill(parsed);
102
105
  }
103
106
  }
104
107
  function readInstalledVersion() {
@@ -43,6 +43,11 @@ export type ParsedCli = {
43
43
  } | {
44
44
  kind: 'retrospective-transcript';
45
45
  args: string[];
46
+ } | {
47
+ kind: 'link-skill';
48
+ name: string;
49
+ sourceRoot: string;
50
+ targetRoot: string;
46
51
  } | ParsedGhaRuntimeAudit | ParsedGhaArtifactsCleanup;
47
52
  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';
48
53
  export declare function parseCli(argv: readonly string[]): ParsedCli;
@@ -50,6 +50,8 @@ export function parseCli(argv) {
50
50
  return parseHttpOrigin(rest);
51
51
  if (command === 'retrospective-transcript')
52
52
  return { kind: 'retrospective-transcript', args: rest };
53
+ if (command === 'link-skill')
54
+ return parseLinkSkill(rest);
53
55
  if (command === 'gha-artifacts-cleanup')
54
56
  return parseGhaArtifactsCleanup(rest);
55
57
  if (command !== undefined && SCRIPT_COMMANDS.has(command)) {
@@ -57,6 +59,29 @@ export function parseCli(argv) {
57
59
  }
58
60
  return { kind: 'error', message: `unknown command: ${command}` };
59
61
  }
62
+ function parseLinkSkill(args) {
63
+ const [name, ...flags] = args;
64
+ if (name === undefined || name.startsWith('-'))
65
+ return { kind: 'error', message: 'link-skill requires a skill name' };
66
+ let sourceRoot;
67
+ let targetRoot;
68
+ for (let index = 0; index < flags.length; index += 1) {
69
+ const flag = flags[index];
70
+ const value = flags[index + 1];
71
+ if (flag !== '--source-root' && flag !== '--target-root')
72
+ return { kind: 'error', message: `unknown link-skill option: ${flag}` };
73
+ if (value === undefined)
74
+ return { kind: 'error', message: `${flag} requires a path` };
75
+ if (flag === '--source-root')
76
+ sourceRoot = value;
77
+ else
78
+ targetRoot = value;
79
+ index += 1;
80
+ }
81
+ if (sourceRoot === undefined || targetRoot === undefined)
82
+ return { kind: 'error', message: 'link-skill requires --source-root and --target-root' };
83
+ return { kind: 'link-skill', name, sourceRoot, targetRoot };
84
+ }
60
85
  function parseRunnerPortPolicy(args) {
61
86
  let file;
62
87
  let reserved;
@@ -1,2 +1,2 @@
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 gha-runtime-audit Audit successful GitHub Actions job runtimes\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 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 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\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\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\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]\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>...\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]\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 gha-runtime-audit Audit successful GitHub Actions job runtimes\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 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 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\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\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\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]\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>...\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>\n";
2
2
  export declare function printUsage(stream?: NodeJS.WritableStream): void;
@@ -32,6 +32,7 @@ Commands:
32
32
  post-review Post one COMMENT review from a staged payload file
33
33
  stage-review-payload Validate a review payload file into a staging directory
34
34
  retrospective-transcript Format facts from Claude-compatible, Codex, or Grok transcripts
35
+ link-skill Link one packaged skill into an explicit consumer directory
35
36
 
36
37
  Options:
37
38
  -h, --help Show this help
@@ -86,6 +87,7 @@ swift-semantic-equal <base> <head> <file.swift>
86
87
  post-review
87
88
  stage-review-payload optional|required <source> <destination>
88
89
  retrospective-transcript [--session-id ID] [--jsonl PATH] [--projects-dir PATH] [--codex-sessions-dir PATH] [--grok-sessions-dir PATH]
90
+ link-skill <name> --source-root <skills-dir> --target-root <consumer-skills-dir>
89
91
  `;
90
92
  export function printUsage(stream = process.stdout) {
91
93
  stream.write(USAGE);
@@ -14,6 +14,8 @@ export declare function createGhPostReviewIo(options: {
14
14
  payloadBytes: Buffer;
15
15
  token: string;
16
16
  exec: GhExec;
17
+ expectedHeadSha?: string;
18
+ expectedBaseSha?: string;
17
19
  }): PostReviewIo;
18
20
  export declare function postReviewFromEnv(env?: NodeJS.ProcessEnv, exec?: GhExec, claudeIo?: import("./claude-token.mts").ClaudeTokenIo): Promise<{
19
21
  posted: boolean;
@@ -33,8 +33,25 @@ export function writePostedOutput(posted, outputPath = process.env.GITHUB_OUTPUT
33
33
  return;
34
34
  appendFileSync(outputPath, `posted=${posted ? 'true' : 'false'}\n`);
35
35
  }
36
+ function readPullRefs(repository, prNumber, exec) {
37
+ let lastError;
38
+ for (let attempt = 1; attempt <= 3; attempt += 1) {
39
+ try {
40
+ return exec([
41
+ 'api',
42
+ `repos/${repository}/pulls/${prNumber}`,
43
+ '--jq',
44
+ '[.head.sha, .base.sha] | @tsv',
45
+ ]).split('\t');
46
+ }
47
+ catch (error) {
48
+ lastError = error;
49
+ }
50
+ }
51
+ throw lastError;
52
+ }
36
53
  export function createGhPostReviewIo(options) {
37
- const { repository, prNumber, payloadBytes, token, exec } = options;
54
+ const { repository, prNumber, payloadBytes, token, exec, expectedHeadSha, expectedBaseSha } = options;
38
55
  return {
39
56
  readFile() {
40
57
  return payloadBytes;
@@ -43,11 +60,34 @@ export function createGhPostReviewIo(options) {
43
60
  rmSync(path, { force: true });
44
61
  },
45
62
  getHeadSha() {
46
- const sha = exec(['api', `repos/${repository}/pulls/${prNumber}`, '--jq', '.head.sha']);
47
- if (!/^[0-9a-f]{40}$/u.test(sha)) {
48
- throw new ReviewPayloadError(`Could not resolve PR head SHA (got "${sha}").`);
63
+ if (!/^[1-9][0-9]*$/u.test(prNumber)) {
64
+ throw new ReviewPayloadError('PR_NUMBER must be a positive integer.');
65
+ }
66
+ if (Boolean(expectedHeadSha) !== Boolean(expectedBaseSha)) {
67
+ throw new ReviewPayloadError('EXPECTED_HEAD_SHA and EXPECTED_BASE_SHA must be provided together.');
68
+ }
69
+ if (expectedHeadSha && !/^[0-9a-f]{40}$/u.test(expectedHeadSha)) {
70
+ throw new ReviewPayloadError('EXPECTED_HEAD_SHA must be a full lowercase commit SHA.');
71
+ }
72
+ if (expectedBaseSha && !/^[0-9a-f]{40}$/u.test(expectedBaseSha)) {
73
+ throw new ReviewPayloadError('EXPECTED_BASE_SHA must be a full lowercase commit SHA.');
74
+ }
75
+ const refs = readPullRefs(repository, prNumber, exec);
76
+ const [headSha = '', baseSha = ''] = refs;
77
+ if (!/^[0-9a-f]{40}$/u.test(headSha)) {
78
+ throw new ReviewPayloadError(`Could not resolve PR head SHA (got "${headSha}").`);
79
+ }
80
+ if (!/^[0-9a-f]{40}$/u.test(baseSha)) {
81
+ throw new ReviewPayloadError(`Could not resolve PR base SHA (got "${baseSha}").`);
82
+ }
83
+ if (expectedHeadSha && headSha !== expectedHeadSha) {
84
+ throw new ReviewPayloadError('PR head changed before posting the selected review.');
85
+ }
86
+ if (expectedBaseSha && baseSha !== expectedBaseSha) {
87
+ throw new ReviewPayloadError('PR base changed before posting the selected review.');
49
88
  }
50
- return sha;
89
+ // Preserve the orchestrator-selected revision as the review commit_id after equality checks.
90
+ return expectedHeadSha || headSha;
51
91
  },
52
92
  listPullFiles() {
53
93
  return parseReviewFilesJson(exec(['api', '--paginate', `repos/${repository}/pulls/${prNumber}/files?per_page=100`]));
@@ -69,6 +109,8 @@ export async function postReviewFromEnv(env = process.env, exec = createGhExec()
69
109
  payloadBytes,
70
110
  token,
71
111
  exec,
112
+ expectedHeadSha: env.EXPECTED_HEAD_SHA ?? '',
113
+ expectedBaseSha: env.EXPECTED_BASE_SHA ?? '',
72
114
  }));
73
115
  const token = resolveReviewPostToken(env);
74
116
  if (token.source === 'github-token')
@@ -2,13 +2,18 @@ import { ReviewPayloadError, indexReviewFiles, parseReviewPayload, remapReviewCo
2
2
  export { MAX_REVIEW_COMMENTS as MAX_COMMENTS, MAX_REVIEW_PAYLOAD_BYTES as MAX_PAYLOAD_BYTES, ReviewPayloadError as PostReviewError, } from '../gha-review-payload/index.mjs';
3
3
  export function runPostReview(payloadPath, io) {
4
4
  try {
5
- let review = parseReviewPayload(io.readFile(payloadPath), io.getHeadSha());
5
+ const selectedHeadSha = io.getHeadSha();
6
+ let review = parseReviewPayload(io.readFile(payloadPath), selectedHeadSha);
6
7
  try {
7
8
  review = remapReviewComments(review, indexReviewFiles(io.listPullFiles()));
8
9
  }
9
10
  catch {
10
11
  // Keep the parsed review when the PR file list is unavailable.
11
12
  }
13
+ // The adapter also revalidates a pinned base; equality catches legacy live-head drift.
14
+ if (io.getHeadSha() !== selectedHeadSha) {
15
+ throw new ReviewPayloadError('PR head changed while preparing the selected review.');
16
+ }
12
17
  const first = io.postReview(review);
13
18
  if (first.ok)
14
19
  return { posted: true };
package/dist/index.d.mts CHANGED
@@ -1,3 +1,5 @@
1
+ export { linkSkill, readSkillManifest } from './skill-discovery/index.mts';
2
+ export type { LinkSkillOptions, LinkSkillResult, SkillManifest, SkillManifestEntry, } from './skill-discovery/index.mts';
1
3
  export { codexChildren, codexIdentity, computeTranscriptFacts, formatTranscriptFacts, formatUnavailable, resolveTranscriptFile, runRetrospectiveTranscript, } from './retrospective-transcript/index.mts';
2
4
  export type { ResolveOptions, TokenTotals, TranscriptFacts, } from './retrospective-transcript/index.mts';
3
5
  export { buildSessionFrictionReport, classifyFrictionObservation, FRICTION_LOG_MAX_EVENTS, isConformingCiFailureBlock, normalizeCommandPrefix, readFrictionLog, recordFriction, } from './session-friction/index.mts';
package/dist/index.mjs CHANGED
@@ -1,4 +1,5 @@
1
1
  /* eslint-disable max-lines -- package entry point enumerates the supported public API. */
2
+ export { linkSkill, readSkillManifest } from './skill-discovery/index.mjs';
2
3
  export { codexChildren, codexIdentity, computeTranscriptFacts, formatTranscriptFacts, formatUnavailable, resolveTranscriptFile, runRetrospectiveTranscript, } from './retrospective-transcript/index.mjs';
3
4
  export { buildSessionFrictionReport, classifyFrictionObservation, FRICTION_LOG_MAX_EVENTS, isConformingCiFailureBlock, normalizeCommandPrefix, readFrictionLog, recordFriction, } from './session-friction/index.mjs';
4
5
  export { EphemeralListenerAttemptsExhaustedError, isRunnerReservedPort, listenOnRunnerUnreservedEphemeralPort, loadRunnerPortPolicy, runnerPortPolicy, validateRunnerPortPolicy, } from './runner-port-policy/index.mjs';
@@ -6,4 +6,5 @@ export { formatReleaseAgeFailure, isReleaseAgeViolation, parseReleaseAgeViolatio
6
6
  export { flattenReleaseAgeSelectors, packageNameFromPnpmLockKey, pnpmLockPackageKeyMatchesSelector, validateReleaseAgeExemptionGroups, validateReleaseAgePolicy, } from './release-age-policy.mts';
7
7
  export type { ReleaseAgeExemptionGroup, ReleaseAgePermanentExemption, ReleaseAgePolicyConfig, ReleaseAgePolicySnapshot, } from './release-age-policy.mts';
8
8
  export { INSTALL_TERMINATION_FAILED, installExitCode, safeProcessGroup, startInstallHeartbeat, terminateProcessGroup, terminateSafeProcessGroup, } from './process.mts';
9
- export { persistentDependencyTreeIsCold, persistentMetadataFingerprint, persistentMetadataMatches, writePersistentMetadataStamp, } from './metadata.mts';
9
+ export { persistentDependencyTreeIsCold } from './metadata.mts';
10
+ export { persistentMetadataFingerprint, persistentMetadataMatches, writePersistentMetadataStamp, } from './metadata-legacy.mts';
@@ -4,4 +4,5 @@ export { baseInstallArgs, findWorkspaceLinkMismatches, listWorkspaces, logWorksp
4
4
  export { formatReleaseAgeFailure, isReleaseAgeViolation, parseReleaseAgeViolations, } from './release-age.mjs';
5
5
  export { flattenReleaseAgeSelectors, packageNameFromPnpmLockKey, pnpmLockPackageKeyMatchesSelector, validateReleaseAgeExemptionGroups, validateReleaseAgePolicy, } from './release-age-policy.mjs';
6
6
  export { INSTALL_TERMINATION_FAILED, installExitCode, safeProcessGroup, startInstallHeartbeat, terminateProcessGroup, terminateSafeProcessGroup, } from './process.mjs';
7
- export { persistentDependencyTreeIsCold, persistentMetadataFingerprint, persistentMetadataMatches, writePersistentMetadataStamp, } from './metadata.mjs';
7
+ export { persistentDependencyTreeIsCold } from './metadata.mjs';
8
+ export { persistentMetadataFingerprint, persistentMetadataMatches, writePersistentMetadataStamp, } from './metadata-legacy.mjs';
@@ -0,0 +1,4 @@
1
+ import { type CommandResult, type InstallOptions } from './support.mts';
2
+ export declare function withScriptPolicy(args: string[], installScripts: boolean): string[];
3
+ export declare function install(args: string[], options: InstallOptions, label: string): Promise<void>;
4
+ export declare function reconcileOrFail(options: InstallOptions, runCapture: (args: string[]) => Promise<CommandResult>): Promise<void>;
@@ -0,0 +1,38 @@
1
+ import { scheduler } from 'node:timers/promises';
2
+ import { runPnpm } from './exec.mjs';
3
+ import { INSTALL_TERMINATION_FAILED } from './process.mjs';
4
+ import { formatReleaseAgeFailure, isReleaseAgeViolation } from './release-age.mjs';
5
+ import { baseInstallArgs, findWorkspaceLinkMismatches, logWorkspaceLinkMismatches, } from './support.mjs';
6
+ function fail(message) {
7
+ throw new Error(message);
8
+ }
9
+ export function withScriptPolicy(args, installScripts) {
10
+ return installScripts ? args : [...args, '--ignore-scripts'];
11
+ }
12
+ export async function install(args, options, label) {
13
+ for (let attempt = 1; attempt <= options.maxAttempts; attempt += 1) {
14
+ const result = await runPnpm(args, options);
15
+ if (result.code === 0)
16
+ return;
17
+ if (result.code === INSTALL_TERMINATION_FAILED)
18
+ fail(`${label} could not terminate safely`);
19
+ const output = `${result.output}\n${result.errorOutput ?? ''}`;
20
+ if (isReleaseAgeViolation(output))
21
+ fail(formatReleaseAgeFailure(label, output));
22
+ if (attempt < options.maxAttempts) {
23
+ console.warn(`${label} failed (attempt ${attempt}/${options.maxAttempts}); retrying`);
24
+ await scheduler.wait(5000);
25
+ }
26
+ }
27
+ fail(`${label} failed after ${options.maxAttempts} attempt${options.maxAttempts === 1 ? '' : 's'}`);
28
+ }
29
+ export async function reconcileOrFail(options, runCapture) {
30
+ const forced = ['install', '--frozen-lockfile', '--force', ...baseInstallArgs.slice(2)];
31
+ await install([...forced, '--ignore-scripts', '--ignore-pnpmfile'], options, 'script-free reconciliation');
32
+ await install(withScriptPolicy(forced, options.installScripts), options, 'strict persistent reconciliation');
33
+ const remaining = await findWorkspaceLinkMismatches(runCapture);
34
+ if (remaining.length > 0) {
35
+ logWorkspaceLinkMismatches(remaining);
36
+ fail('persistent reconciliation completed with invalid workspace links');
37
+ }
38
+ }
@@ -0,0 +1,4 @@
1
+ import { type CaptureCommand } from './support.mts';
2
+ export declare function persistentMetadataFingerprint(runCapture: CaptureCommand, installScripts: boolean): Promise<string>;
3
+ export declare function persistentMetadataMatches(fingerprint: string): Promise<boolean>;
4
+ export declare function writePersistentMetadataStamp(fingerprint: string): Promise<void>;
@@ -0,0 +1,81 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { listWorkspaces, reportGlibcVersionRuntime } from './support.mjs';
5
+ function persistentMetadataStampPath() {
6
+ return path.join(process.cwd(), 'node_modules', '.pnpm-install-metadata-health.v3.json');
7
+ }
8
+ function legacySharedStampPath() {
9
+ return path.join(process.cwd(), 'node_modules', '.pnpm-install-metadata-health.json');
10
+ }
11
+ function addFingerprintInput(hash, label, value) {
12
+ hash.update(`${label.length}:${label}${value.length}:${value}`);
13
+ }
14
+ async function optionalFile(pathname) {
15
+ try {
16
+ return await readFile(pathname, 'utf8');
17
+ }
18
+ catch (error) {
19
+ if (error.code === 'ENOENT')
20
+ return '';
21
+ throw error;
22
+ }
23
+ }
24
+ function runtimePlatformIdentity() {
25
+ const report = process.report?.getReport();
26
+ return JSON.stringify({
27
+ arch: process.arch,
28
+ glibc: reportGlibcVersionRuntime(report),
29
+ modules: process.versions.modules,
30
+ node: process.version,
31
+ npmConfigArch: process.env.npm_config_arch ?? '',
32
+ npmConfigLibc: process.env.npm_config_libc ?? '',
33
+ npmConfigPlatform: process.env.npm_config_platform ?? '',
34
+ platform: process.platform,
35
+ });
36
+ }
37
+ export async function persistentMetadataFingerprint(runCapture, installScripts) {
38
+ const workspaces = (await listWorkspaces(runCapture)).toSorted((left, right) => left.path.localeCompare(right.path));
39
+ const pnpmVersion = await runCapture(['--version']);
40
+ if (pnpmVersion.code !== 0)
41
+ throw new Error(`pnpm --version failed: ${pnpmVersion.errorOutput?.trim() || pnpmVersion.output.trim() || 'unknown error'}`);
42
+ const hash = createHash('sha256');
43
+ addFingerprintInput(hash, 'runtime', runtimePlatformIdentity());
44
+ addFingerprintInput(hash, 'pnpm', pnpmVersion.output.trim());
45
+ addFingerprintInput(hash, 'installScripts', String(installScripts));
46
+ for (const filename of [
47
+ 'pnpm-lock.yaml',
48
+ 'pnpm-workspace.yaml',
49
+ '.npmrc',
50
+ '.pnpmfile.cjs',
51
+ '.pnpmfile.mjs',
52
+ ])
53
+ addFingerprintInput(hash, filename, await optionalFile(path.join(process.cwd(), filename)));
54
+ for (const workspace of workspaces)
55
+ addFingerprintInput(hash, path.relative(process.cwd(), path.join(workspace.path, 'package.json')), await readFile(path.join(workspace.path, 'package.json'), 'utf8'));
56
+ return hash.digest('hex');
57
+ }
58
+ export async function persistentMetadataMatches(fingerprint) {
59
+ for (const filename of [persistentMetadataStampPath(), legacySharedStampPath()]) {
60
+ try {
61
+ const stamp = JSON.parse(await readFile(filename, 'utf8'));
62
+ if (stamp.version === 3 && stamp.fingerprint === fingerprint)
63
+ return true;
64
+ }
65
+ catch { }
66
+ }
67
+ return false;
68
+ }
69
+ export async function writePersistentMetadataStamp(fingerprint) {
70
+ const stampPath = persistentMetadataStampPath();
71
+ const directory = path.dirname(stampPath);
72
+ const temporary = `${stampPath}.${process.pid}.tmp`;
73
+ await mkdir(directory, { recursive: true });
74
+ try {
75
+ await writeFile(temporary, `${JSON.stringify({ fingerprint, version: 3 })}\n`);
76
+ await rename(temporary, stampPath);
77
+ }
78
+ finally {
79
+ await rm(temporary, { force: true });
80
+ }
81
+ }
@@ -1,5 +1,18 @@
1
1
  import { type CaptureCommand } from './support.mts';
2
- export declare function persistentMetadataFingerprint(runCapture: CaptureCommand, installScripts: boolean): Promise<string>;
3
- export declare function persistentMetadataMatches(fingerprint: string): Promise<boolean>;
2
+ import { type ProvenanceStatus } from './transition.mts';
3
+ declare const componentNames: readonly ['lockfile', 'npm-config', 'pnpm', 'pnpmfiles', 'runtime', 'workspace-config', 'workspace-manifests'];
4
+ type ComponentName = (typeof componentNames)[number];
5
+ type StructuralProvenance = Record<ComponentName, string>;
6
+ export declare function persistentMetadataFingerprintV4(runCapture: CaptureCommand): Promise<{
7
+ lockfile: string;
8
+ 'npm-config': string;
9
+ pnpm: string;
10
+ pnpmfiles: string;
11
+ runtime: string;
12
+ 'workspace-config': string;
13
+ 'workspace-manifests': string;
14
+ }>;
15
+ export declare function persistentMetadataStatusV4(provenance: StructuralProvenance): Promise<ProvenanceStatus>;
16
+ export declare function writePersistentMetadataStampV4(provenance: StructuralProvenance, installScripts: boolean, resetScriptsEnabledCapability: boolean, pendingDependencyBuilds?: string[]): Promise<void>;
4
17
  export declare function persistentDependencyTreeIsCold(): Promise<boolean>;
5
- export declare function writePersistentMetadataStamp(fingerprint: string): Promise<void>;
18
+ export {};