@tryinget/pi-agent-vent 0.1.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.
package/package.json ADDED
@@ -0,0 +1,95 @@
1
+ {
2
+ "name": "@tryinget/pi-agent-vent",
3
+ "version": "0.1.0",
4
+ "description": "Local pi tool for agents to record recurring frustrations, bugs, and workflow friction without creating incidents or external telemetry",
5
+ "type": "module",
6
+ "license": "SEE LICENSE IN LICENSE",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/tryingET/pi-extensions.git",
10
+ "directory": "packages/pi-agent-vent"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/tryingET/pi-extensions/issues"
14
+ },
15
+ "homepage": "https://github.com/tryingET/pi-extensions/tree/main/packages/pi-agent-vent",
16
+ "keywords": [
17
+ "pi-package",
18
+ "pi-extension",
19
+ "agent-vent",
20
+ "monorepo",
21
+ "agent-feedback",
22
+ "frustration-capture",
23
+ "local-diagnostics",
24
+ "incident-candidate"
25
+ ],
26
+ "publishConfig": {
27
+ "registry": "https://registry.npmjs.org/",
28
+ "access": "public"
29
+ },
30
+ "engines": {
31
+ "node": ">=22"
32
+ },
33
+ "scripts": {
34
+ "fix": "bash ./scripts/quality-gate.sh fix",
35
+ "lint": "bash ./scripts/quality-gate.sh lint",
36
+ "typecheck": "bash ./scripts/quality-gate.sh typecheck",
37
+ "quality:pre-commit": "bash ./scripts/quality-gate.sh pre-commit",
38
+ "quality:pre-push": "bash ./scripts/quality-gate.sh pre-push",
39
+ "quality:ci": "bash ./scripts/quality-gate.sh ci",
40
+ "check": "npm run quality:ci",
41
+ "test": "npm run quality:ci",
42
+ "docs:list": "bash ./scripts/docs-list.sh",
43
+ "docs:list:workspace": "bash ./scripts/docs-list.sh --workspace --discover",
44
+ "docs:list:json": "bash ./scripts/docs-list.sh --json",
45
+ "release:check": "bash ./scripts/release-check.sh",
46
+ "release:check:quick": "SKIP_PI_SMOKE=1 bash ./scripts/release-check.sh"
47
+ },
48
+ "files": [
49
+ "extensions/agent-vent.ts",
50
+ "src/vent-store.js",
51
+ "scripts",
52
+ "tests",
53
+ "biome.jsonc",
54
+ "prompts",
55
+ "examples",
56
+ "policy/security-policy.json",
57
+ "policy/engineering-lane.json",
58
+ "docs/engineering.local.md",
59
+ "docs/project/vision.md",
60
+ "docs/project/product-posture.md",
61
+ "docs/project/2026-05-21-agent-vent-design.md",
62
+ "docs/project/2026-05-21-agent-vent-implementation-plan.md",
63
+ "docs/project/extension-sop.md",
64
+ "docs/project/trusted-publishing.md",
65
+ "docs/adr/2026-05-22-agent-vent-retention-delete-policy.md"
66
+ ],
67
+ "pi": {
68
+ "extensions": [
69
+ "./extensions/agent-vent.ts"
70
+ ],
71
+ "prompts": [
72
+ "./prompts"
73
+ ]
74
+ },
75
+ "x-pi-template": {
76
+ "scaffoldMode": "simple-package",
77
+ "workspacePath": "packages/pi-agent-vent",
78
+ "releaseComponent": "pi-agent-vent",
79
+ "releaseConfigMode": "component"
80
+ },
81
+ "devDependencies": {
82
+ "@biomejs/biome": "2.3.14"
83
+ },
84
+ "overrides": {
85
+ "fast-xml-parser": "5.3.6"
86
+ },
87
+ "peerDependencies": {
88
+ "@mariozechner/pi-ai": "*",
89
+ "@mariozechner/pi-coding-agent": "*",
90
+ "typebox": "*"
91
+ },
92
+ "dependencies": {
93
+ "typebox": "^1.0.0"
94
+ }
95
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "lane": "ts",
3
+ "engineering_core": {
4
+ "tool": "engineering-core",
5
+ "lane": "pi-ts",
6
+ "repository": "https://github.com/tryingET/core_engineering-core",
7
+ "ref": "workspace-local-unpinned",
8
+ "command": "uv tool -n run --from ~/ai-society/core/engineering-core engineering-core show pi-ts",
9
+ "catalog_command": "uv tool -n run --from ~/ai-society/core/engineering-core engineering-core catalog --pretty",
10
+ "list_disciplines_command": "uv tool -n run --from ~/ai-society/core/engineering-core engineering-core list-disciplines",
11
+ "list_templates_command": "uv tool -n run --from ~/ai-society/core/engineering-core engineering-core list-templates",
12
+ "disciplines": [
13
+ "validation",
14
+ "testing",
15
+ "security-privacy",
16
+ "documentation",
17
+ "dependency-governance",
18
+ "specification-and-dsls",
19
+ "engineering-reasoning"
20
+ ]
21
+ }
22
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "default": {
3
+ "maxRiskScore": 70,
4
+ "requireIntegrity": true,
5
+ "requireSignatures": false,
6
+ "allowNpmDiffFallback": false,
7
+ "minReleaseAgeHours": 0
8
+ },
9
+ "packages": {}
10
+ }
@@ -0,0 +1,20 @@
1
+ ---
2
+ summary: "Monorepo package implementation planning prompt template."
3
+ read_when:
4
+ - "Using or updating the monorepo package implementation-planning prompt template."
5
+ description: Draft an implementation plan for a requested change
6
+ system4d:
7
+ container: "Prompt template for implementation planning."
8
+ compass: "Turn requests into actionable, risk-aware plans."
9
+ engine: "Scope -> tasks -> validation -> rollout."
10
+ fog: "Hidden constraints unless assumptions are surfaced."
11
+ ---
12
+
13
+ Create an implementation plan for this request: $@
14
+
15
+ Include:
16
+ - Scope and non-goals
17
+ - Key risks and mitigations
18
+ - Step-by-step implementation tasks
19
+ - Validation commands and expected outcomes
20
+ - Rollout and rollback notes
@@ -0,0 +1,20 @@
1
+ ---
2
+ summary: "Monorepo package security review prompt template."
3
+ read_when:
4
+ - "Using or updating the monorepo package security-review prompt template."
5
+ description: Review a change for security risks and mitigations
6
+ system4d:
7
+ container: "Prompt template for security-focused review."
8
+ compass: "Identify practical vulnerabilities before release."
9
+ engine: "Threats -> impact -> mitigations -> verification."
10
+ fog: "Partial context can hide exploit paths."
11
+ ---
12
+
13
+ Review this change for security concerns: $@
14
+
15
+ Focus on:
16
+ - Input validation and injection risk
17
+ - Privilege boundaries and secret handling
18
+ - Dependency and supply-chain risk
19
+ - Safe failure modes and logging
20
+ - Concrete remediations with priority
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5
+ DEFAULT_SCRIPT="$HOME/ai-society/core/agent-scripts/scripts/docs-list.mjs"
6
+ LOCAL_FALLBACK_SCRIPT="$ROOT_DIR/scripts/docs-list.mjs"
7
+
8
+ usage() {
9
+ cat <<'USAGE'
10
+ Usage: ./scripts/docs-list.sh [docs-list args]
11
+
12
+ Resolves docs-list script in this order:
13
+ 1) DOCS_LIST_SCRIPT env var (absolute/relative path)
14
+ 2) Local fallback: ./scripts/docs-list.mjs
15
+ 3) Default global path: ~/ai-society/core/agent-scripts/scripts/docs-list.mjs
16
+
17
+ Examples:
18
+ ./scripts/docs-list.sh
19
+ ./scripts/docs-list.sh --workspace --discover
20
+ ./scripts/docs-list.sh --json
21
+ USAGE
22
+ }
23
+
24
+ if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
25
+ usage
26
+ exit 0
27
+ fi
28
+
29
+ if ! command -v node >/dev/null 2>&1; then
30
+ echo "Error: node is required to run docs-list." >&2
31
+ exit 1
32
+ fi
33
+
34
+ SCRIPT_PATH=""
35
+
36
+ if [[ -n "${DOCS_LIST_SCRIPT:-}" ]]; then
37
+ SCRIPT_PATH="${DOCS_LIST_SCRIPT}"
38
+ elif [[ -f "$LOCAL_FALLBACK_SCRIPT" ]]; then
39
+ SCRIPT_PATH="$LOCAL_FALLBACK_SCRIPT"
40
+ else
41
+ SCRIPT_PATH="$DEFAULT_SCRIPT"
42
+ fi
43
+
44
+ if [[ ! -f "$SCRIPT_PATH" ]]; then
45
+ echo "Error: docs-list script not found: $SCRIPT_PATH" >&2
46
+ echo "Set DOCS_LIST_SCRIPT to your docs-list.mjs path, or place docs-list script at: $DEFAULT_SCRIPT" >&2
47
+ exit 1
48
+ fi
49
+
50
+ node "$SCRIPT_PATH" "$@"
@@ -0,0 +1,168 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5
+ SEARCH_DIR="$ROOT_DIR"
6
+ OVERRIDE_GATE="${PACKAGE_QUALITY_GATE_SCRIPT:-}"
7
+
8
+ if [[ -n "$OVERRIDE_GATE" ]]; then
9
+ if [[ -f "$OVERRIDE_GATE" ]]; then
10
+ exec bash "$OVERRIDE_GATE" "${1:-}" "$ROOT_DIR"
11
+ fi
12
+ echo "error: PACKAGE_QUALITY_GATE_SCRIPT does not exist: $OVERRIDE_GATE" >&2
13
+ exit 1
14
+ fi
15
+
16
+ while [[ "$SEARCH_DIR" != "/" ]]; do
17
+ if [[ -x "$SEARCH_DIR/scripts/package-quality-gate.sh" ]]; then
18
+ exec bash "$SEARCH_DIR/scripts/package-quality-gate.sh" "${1:-}" "$ROOT_DIR"
19
+ fi
20
+ SEARCH_DIR="$(dirname "$SEARCH_DIR")"
21
+ done
22
+
23
+ stage="${1:-}"
24
+ case "$stage" in
25
+ lint|fix|typecheck|test|pre-commit|pre-push|ci) ;;
26
+ *)
27
+ echo "Usage: bash ./scripts/quality-gate.sh <lint|fix|typecheck|test|pre-commit|pre-push|ci>" >&2
28
+ exit 1
29
+ ;;
30
+ esac
31
+
32
+ relative_root() {
33
+ basename "$ROOT_DIR"
34
+ }
35
+
36
+ has_biome_config() {
37
+ [[ -f "$ROOT_DIR/biome.json" ]] || [[ -f "$ROOT_DIR/biome.jsonc" ]]
38
+ }
39
+
40
+ run_biome() {
41
+ if ! has_biome_config; then
42
+ echo "lint: skipped ($(relative_root), no biome config found)"
43
+ return 0
44
+ fi
45
+ if [[ ! -x "$ROOT_DIR/node_modules/.bin/biome" ]]; then
46
+ echo "biome: skipped ($(relative_root), local biome binary unavailable; run npm install to enable lint)."
47
+ return 0
48
+ fi
49
+
50
+ # Runtime tarballs do not necessarily include a VCS ignore file. The package biome config
51
+ # already excludes node_modules/dist/coverage, so disable VCS-ignore-file loading there.
52
+ local -a vcs_args=()
53
+ if [[ ! -f "$ROOT_DIR/.gitignore" ]]; then
54
+ vcs_args+=(--vcs-use-ignore-file=false)
55
+ fi
56
+
57
+ (cd "$ROOT_DIR" && ./node_modules/.bin/biome "$@" "${vcs_args[@]}")
58
+ }
59
+
60
+ run_lint() {
61
+ run_biome check --no-errors-on-unmatched .
62
+ }
63
+
64
+ run_fix() {
65
+ run_biome check --write --no-errors-on-unmatched .
66
+ }
67
+
68
+ run_typecheck() {
69
+ if [[ ! -f "$ROOT_DIR/tsconfig.json" ]]; then
70
+ echo "typecheck: skipped ($(relative_root), no tsconfig.json found)"
71
+ return 0
72
+ fi
73
+ if [[ -x "$ROOT_DIR/node_modules/.bin/tsgo" ]]; then
74
+ (cd "$ROOT_DIR" && ./node_modules/.bin/tsgo --noEmit)
75
+ return 0
76
+ fi
77
+ if [[ -x "$ROOT_DIR/node_modules/.bin/tsc" ]]; then
78
+ (cd "$ROOT_DIR" && ./node_modules/.bin/tsc --noEmit)
79
+ return 0
80
+ fi
81
+ echo "typecheck: skipped ($(relative_root), tsconfig found but no local tsgo/tsc binary)."
82
+ }
83
+
84
+ run_tests() {
85
+ if [[ ! -d "$ROOT_DIR/tests" ]]; then
86
+ echo "tests: skipped ($(relative_root), no tests directory found)"
87
+ return 0
88
+ fi
89
+ mapfile -t test_files < <(
90
+ cd "$ROOT_DIR"
91
+ find tests -type f \( \
92
+ -name '*.test.js' -o \
93
+ -name '*.test.mjs' -o \
94
+ -name '*.test.cjs' -o \
95
+ -name '*.test.ts' -o \
96
+ -name '*.test.tsx' -o \
97
+ -name '*.test.mts' -o \
98
+ -name '*.test.cts' \
99
+ \) | LC_ALL=C sort
100
+ )
101
+ if [[ "${#test_files[@]}" -eq 0 ]]; then
102
+ echo "tests: skipped ($(relative_root), no test files found)"
103
+ return 0
104
+ fi
105
+ local needs_tsx="false"
106
+ local test_file
107
+ for test_file in "${test_files[@]}"; do
108
+ case "$test_file" in
109
+ *.test.ts|*.test.tsx|*.test.mts|*.test.cts)
110
+ needs_tsx="true"
111
+ break
112
+ ;;
113
+ esac
114
+ done
115
+
116
+ if [[ "$needs_tsx" == "true" ]]; then
117
+ if [[ ! -x "$ROOT_DIR/node_modules/.bin/tsx" ]]; then
118
+ echo "tests: TypeScript test files detected but local tsx binary is unavailable." >&2
119
+ exit 1
120
+ fi
121
+ (cd "$ROOT_DIR" && node --import tsx --test "${test_files[@]}")
122
+ return 0
123
+ fi
124
+
125
+ (cd "$ROOT_DIR" && node --test "${test_files[@]}")
126
+ }
127
+
128
+ run_structure_if_source_checkout() {
129
+ if [[ -f "$ROOT_DIR/.copier-answers.yml" && -f "$ROOT_DIR/scripts/validate-structure.sh" ]]; then
130
+ (cd "$ROOT_DIR" && bash ./scripts/validate-structure.sh)
131
+ else
132
+ echo "structure: skipped ($(relative_root), source-checkout structure contract not present)"
133
+ fi
134
+ }
135
+
136
+ echo "==> package local quality gate: $(relative_root) [$stage]"
137
+ echo "note: monorepo root gate not found; running self-contained package fallback."
138
+
139
+ case "$stage" in
140
+ lint)
141
+ run_lint
142
+ ;;
143
+ fix)
144
+ run_fix
145
+ ;;
146
+ typecheck)
147
+ run_typecheck
148
+ ;;
149
+ test)
150
+ run_tests
151
+ ;;
152
+ pre-commit)
153
+ run_structure_if_source_checkout
154
+ run_lint
155
+ ;;
156
+ pre-push)
157
+ run_structure_if_source_checkout
158
+ run_lint
159
+ run_typecheck
160
+ run_tests
161
+ ;;
162
+ ci)
163
+ run_structure_if_source_checkout
164
+ run_lint
165
+ run_typecheck
166
+ run_tests
167
+ ;;
168
+ esac
@@ -0,0 +1,221 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ export const normalizePackPath = (value) => String(value).replace(/^\.\//, "").replace(/\\/g, "/");
5
+
6
+ export const assertSafePackPath = (filePath) => {
7
+ if (path.posix.isAbsolute(filePath) || filePath === ".." || filePath.split("/").includes("..")) {
8
+ throw new Error(`npm pack output contains unsafe package path: ${filePath}`);
9
+ }
10
+ return filePath;
11
+ };
12
+
13
+ export const parsePackJson = (packJsonText) => {
14
+ let pack;
15
+ try {
16
+ pack = JSON.parse(packJsonText || "[]");
17
+ } catch (error) {
18
+ throw new Error(`Could not parse npm pack --dry-run --json output: ${error.message}`);
19
+ }
20
+
21
+ if (!Array.isArray(pack) || !pack[0] || !Array.isArray(pack[0].files)) {
22
+ throw new Error("Could not parse npm pack --dry-run --json output.");
23
+ }
24
+
25
+ return pack[0].files
26
+ .map((file) => normalizePackPath(String(file.path || "")))
27
+ .filter(Boolean)
28
+ .map(assertSafePackPath)
29
+ .sort();
30
+ };
31
+
32
+ export const readPackageFilesContract = (packageJsonPath = "package.json") => {
33
+ const pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
34
+ const filesEntries = Array.isArray(pkg.files)
35
+ ? pkg.files.map((entry) => normalizePackPath(String(entry).trim())).filter(Boolean)
36
+ : [];
37
+
38
+ if (filesEntries.length === 0) {
39
+ throw new Error(
40
+ "package.json must define a non-empty files array for deterministic publish artifacts.",
41
+ );
42
+ }
43
+
44
+ return filesEntries;
45
+ };
46
+
47
+ const isAlwaysIncluded = (filePath) =>
48
+ /^README(?:\.[^/]+)?$/i.test(filePath) ||
49
+ /^LICENSE(?:\.[^/]+)?$/i.test(filePath) ||
50
+ /^NOTICE(?:\.[^/]+)?$/i.test(filePath);
51
+
52
+ export const validatePackageFilesWhitelist = ({
53
+ actualFiles,
54
+ filesEntries,
55
+ cwd = process.cwd(),
56
+ }) => {
57
+ const actualSet = new Set(actualFiles);
58
+ const expectedExact = new Set(["package.json"]);
59
+ const expectedDirPrefixes = [];
60
+
61
+ for (const entry of filesEntries) {
62
+ if (/[*?[]/.test(entry)) {
63
+ throw new Error(
64
+ `Unsupported files[] wildcard entry; use exact files or directories: ${entry}`,
65
+ );
66
+ }
67
+
68
+ const fullPath = path.resolve(cwd, entry);
69
+ if (!fs.existsSync(fullPath)) {
70
+ throw new Error(`files[] entry does not exist: ${entry}`);
71
+ }
72
+
73
+ const stat = fs.statSync(fullPath);
74
+ if (stat.isDirectory()) {
75
+ expectedDirPrefixes.push(entry.endsWith("/") ? entry : `${entry}/`);
76
+ } else {
77
+ expectedExact.add(entry);
78
+ }
79
+ }
80
+
81
+ const missing = [];
82
+ for (const filePath of expectedExact) {
83
+ if (!actualSet.has(filePath)) {
84
+ missing.push(filePath);
85
+ }
86
+ }
87
+ for (const prefix of expectedDirPrefixes) {
88
+ if (!actualFiles.some((filePath) => filePath.startsWith(prefix))) {
89
+ missing.push(`${prefix}*`);
90
+ }
91
+ }
92
+
93
+ const extra = actualFiles.filter((filePath) => {
94
+ if (expectedExact.has(filePath)) return false;
95
+ if (expectedDirPrefixes.some((prefix) => filePath.startsWith(prefix))) return false;
96
+ if (isAlwaysIncluded(filePath)) return false;
97
+ return true;
98
+ });
99
+
100
+ return { missing, extra };
101
+ };
102
+
103
+ export const isExternalMarkdownDestination = (target) =>
104
+ /^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(target) || target.startsWith("//");
105
+
106
+ export const markdownDestination = (rawTarget) => {
107
+ let target = String(rawTarget || "").trim();
108
+ if (!target) return "";
109
+ if (target.startsWith("<")) {
110
+ const closingAngle = target.indexOf(">");
111
+ target = closingAngle >= 0 ? target.slice(1, closingAngle).trim() : target;
112
+ } else {
113
+ target = target.split(/\s+/, 1)[0] || "";
114
+ }
115
+ try {
116
+ return decodeURI(target);
117
+ } catch {
118
+ return target;
119
+ }
120
+ };
121
+
122
+ const markdownLinkPattern = /!?\[[^\]\n]*\]\(([^)\n]+)\)/g;
123
+ const referenceLinkDefinitionPattern = /^[ \t]{0,3}\[[^\]\n]+\]:[ \t]*(\S.*)$/gm;
124
+
125
+ const collectMarkdownDestinations = (markdown) => [
126
+ ...[...markdown.matchAll(markdownLinkPattern)].map((match) => match[1]),
127
+ ...[...markdown.matchAll(referenceLinkDefinitionPattern)].map((match) => match[1]),
128
+ ];
129
+
130
+ export const collectPackagedMarkdownLinkFailures = ({ actualFiles, readFile }) => {
131
+ const actualSet = new Set(actualFiles);
132
+ const failures = [];
133
+ const packagedMarkdownFiles = actualFiles.filter((filePath) =>
134
+ filePath.toLowerCase().endsWith(".md"),
135
+ );
136
+
137
+ for (const markdownPath of packagedMarkdownFiles) {
138
+ const markdown = readFile(markdownPath);
139
+ for (const rawTarget of collectMarkdownDestinations(markdown)) {
140
+ const target = markdownDestination(rawTarget);
141
+ if (!target || target.startsWith("#") || isExternalMarkdownDestination(target)) {
142
+ continue;
143
+ }
144
+
145
+ const targetWithoutFragment = target.split("#", 1)[0].split("?", 1)[0];
146
+ if (!targetWithoutFragment) {
147
+ continue;
148
+ }
149
+
150
+ const normalizedTarget = normalizePackPath(
151
+ path.posix.normalize(
152
+ path.posix.join(path.posix.dirname(markdownPath), targetWithoutFragment),
153
+ ),
154
+ );
155
+ if (
156
+ path.posix.isAbsolute(targetWithoutFragment) ||
157
+ normalizedTarget === ".." ||
158
+ normalizedTarget.startsWith("../")
159
+ ) {
160
+ failures.push(`${markdownPath}: local link escapes package root: ${target}`);
161
+ continue;
162
+ }
163
+
164
+ const directoryPrefix = normalizedTarget.endsWith("/")
165
+ ? normalizedTarget
166
+ : `${normalizedTarget}/`;
167
+ const targetIsPackaged =
168
+ actualSet.has(normalizedTarget) ||
169
+ actualSet.has(`${normalizedTarget}.md`) ||
170
+ actualFiles.some((filePath) => filePath.startsWith(directoryPrefix));
171
+ if (!targetIsPackaged) {
172
+ failures.push(`${markdownPath}: local link missing from npm artifact: ${target}`);
173
+ }
174
+ }
175
+ }
176
+
177
+ return { failures, packagedMarkdownCount: packagedMarkdownFiles.length };
178
+ };
179
+
180
+ export const runReleaseArtifactCheck = ({
181
+ packJsonText,
182
+ cwd = process.cwd(),
183
+ log = console.log,
184
+ }) => {
185
+ const actualFiles = parsePackJson(packJsonText);
186
+ const filesEntries = readPackageFilesContract(path.join(cwd, "package.json"));
187
+ const whitelist = validatePackageFilesWhitelist({ actualFiles, filesEntries, cwd });
188
+
189
+ if (whitelist.missing.length || whitelist.extra.length) {
190
+ const lines = ["Publish file whitelist mismatch."];
191
+ if (whitelist.missing.length) lines.push(`Missing: ${whitelist.missing.join(", ")}`);
192
+ if (whitelist.extra.length) lines.push(`Extra: ${whitelist.extra.join(", ")}`);
193
+ throw new Error(lines.join("\n"));
194
+ }
195
+
196
+ const markdown = collectPackagedMarkdownLinkFailures({
197
+ actualFiles,
198
+ readFile: (filePath) => fs.readFileSync(path.join(cwd, filePath), "utf8"),
199
+ });
200
+
201
+ if (markdown.failures.length) {
202
+ throw new Error(
203
+ [
204
+ "Packaged Markdown link check failed.",
205
+ ...markdown.failures.map((failure) => `- ${failure}`),
206
+ ].join("\n"),
207
+ );
208
+ }
209
+
210
+ log(`File whitelist OK (${actualFiles.length} files).`);
211
+ log(`Packaged Markdown links OK (${markdown.packagedMarkdownCount} files).`);
212
+ };
213
+
214
+ if (import.meta.url === `file://${process.argv[1]}`) {
215
+ try {
216
+ runReleaseArtifactCheck({ packJsonText: process.env.PACK_JSON || "[]" });
217
+ } catch (error) {
218
+ console.error(error.message);
219
+ process.exit(1);
220
+ }
221
+ }