@tryinget/pi-agent-vent 0.1.2 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,9 @@
1
+ // ---
2
+ // summary: validates packed file whitelists and local links in published markdown
3
+ // read_when:
4
+ // - changing pi-agent-vent package contents or release artifact policy
5
+ // ---
6
+
1
7
  import fs from "node:fs";
2
8
  import path from "node:path";
3
9
 
@@ -10,7 +16,7 @@ export const assertSafePackPath = (filePath) => {
10
16
  return filePath;
11
17
  };
12
18
 
13
- export const parsePackJson = (packJsonText) => {
19
+ export const parsePackJson = (packJsonText, expectedIdentity = null) => {
14
20
  let pack;
15
21
  try {
16
22
  pack = JSON.parse(packJsonText || "[]");
@@ -18,11 +24,30 @@ export const parsePackJson = (packJsonText) => {
18
24
  throw new Error(`Could not parse npm pack --dry-run --json output: ${error.message}`);
19
25
  }
20
26
 
21
- if (!Array.isArray(pack) || !pack[0] || !Array.isArray(pack[0].files)) {
27
+ let packEntry = null;
28
+ if (Array.isArray(pack) && pack.length === 1) {
29
+ [packEntry] = pack;
30
+ } else if (pack && typeof pack === "object") {
31
+ const entries = Object.entries(pack);
32
+ if (entries.length === 1 && entries[0][0] === entries[0][1]?.name) {
33
+ packEntry = entries[0][1];
34
+ }
35
+ }
36
+ if (!packEntry || !Array.isArray(packEntry.files)) {
22
37
  throw new Error("Could not parse npm pack --dry-run --json output.");
23
38
  }
39
+ if (
40
+ expectedIdentity &&
41
+ (packEntry.name !== expectedIdentity.name ||
42
+ packEntry.version !== expectedIdentity.version ||
43
+ packEntry.id !== `${expectedIdentity.name}@${expectedIdentity.version}`)
44
+ ) {
45
+ throw new Error(
46
+ `npm pack identity mismatch: expected ${expectedIdentity.name}@${expectedIdentity.version}.`,
47
+ );
48
+ }
24
49
 
25
- return pack[0].files
50
+ return packEntry.files
26
51
  .map((file) => normalizePackPath(String(file.path || "")))
27
52
  .filter(Boolean)
28
53
  .map(assertSafePackPath)
@@ -182,8 +207,13 @@ export const runReleaseArtifactCheck = ({
182
207
  cwd = process.cwd(),
183
208
  log = console.log,
184
209
  }) => {
185
- const actualFiles = parsePackJson(packJsonText);
186
- const filesEntries = readPackageFilesContract(path.join(cwd, "package.json"));
210
+ const manifestPath = path.join(cwd, "package.json");
211
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
212
+ const actualFiles = parsePackJson(packJsonText, {
213
+ name: manifest.name,
214
+ version: manifest.version,
215
+ });
216
+ const filesEntries = readPackageFilesContract(manifestPath);
187
217
  const whitelist = validatePackageFilesWhitelist({ actualFiles, filesEntries, cwd });
188
218
 
189
219
  if (whitelist.missing.length || whitelist.extra.length) {
@@ -1,14 +1,121 @@
1
1
  #!/usr/bin/env bash
2
+ # ---
3
+ # summary: exercises pi-agent-vent packaging, isolated installation, and release smoke gates
4
+ # read_when:
5
+ # - preparing a release or investigating tarball and pi installation failures
6
+ # ---
2
7
  set -euo pipefail
3
8
 
4
9
  ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
10
+ REPO_ROOT="$(cd "$ROOT_DIR/../.." && pwd)"
5
11
  cd "$ROOT_DIR"
6
12
 
13
+ # Scratch-root resolution, from first principles:
14
+ # the invariant is "release-check scratch lands on a managed, disposable root",
15
+ # NOT "the TMPDIR variable happens to be exported". A GitHub-hosted runner is
16
+ # single-tenant and ephemeral; its RUNNER_TEMP is exactly a managed scratch
17
+ # root for that job, so CI satisfies the invariant even though runners never
18
+ # export TMPDIR. Locally the strict rule is unchanged: no TMPDIR, no run.
19
+ if [[ -n "${TMPDIR:-}" ]]; then
20
+ TMP_ROOT="$TMPDIR"
21
+ elif [[ "${GITHUB_ACTIONS:-}" == "true" && -n "${RUNNER_TEMP:-}" && -d "${RUNNER_TEMP:-}" ]]; then
22
+ TMP_ROOT="$RUNNER_TEMP"
23
+ else
24
+ echo "TMPDIR must name the managed scratch root for release checks" >&2
25
+ exit 1
26
+ fi
27
+ mkdir -p "$TMP_ROOT"
28
+ TMP_ROOT="$(cd "$TMP_ROOT" && pwd -P)"
29
+ case "$TMP_ROOT" in
30
+ /|"$ROOT_DIR"|"$ROOT_DIR"/*)
31
+ echo "Release check refused unsafe TMPDIR: $TMP_ROOT" >&2
32
+ exit 1
33
+ ;;
34
+ esac
35
+ release_tmp_dir() {
36
+ local label="$1"
37
+ mktemp -d "$TMP_ROOT/pi-agent-vent-${label}-XXXXXX"
38
+ }
39
+
7
40
  NAME="$(node -p "JSON.parse(require('node:fs').readFileSync('package.json', 'utf8')).name")"
8
41
  VERSION="$(node -p "JSON.parse(require('node:fs').readFileSync('package.json', 'utf8')).version")"
9
42
  REPOSITORY_URL="$(node -p "(() => { const pkg = JSON.parse(require('node:fs').readFileSync('package.json', 'utf8')); const repo = pkg.repository; if (typeof repo === 'string') return repo.trim(); if (repo && typeof repo === 'object' && typeof repo.url === 'string') return repo.url.trim(); return ''; })()")"
10
43
 
44
+ HOST_VERSION="$(node -p "JSON.parse(require('node:fs').readFileSync('package.json', 'utf8')).devDependencies['@earendil-works/pi-coding-agent'] || ''")"
45
+
46
+ # One release-only policy membrane. Every dependency-resolving command, including
47
+ # Pi's nested npm invocation, must enter through here with disposable cache/prefix.
48
+ RELEASE_MIN_AGE=0
49
+ with_release_npm_policy() {
50
+ local cache="$1"
51
+ local prefix="$2"
52
+ shift 2
53
+ case "$cache" in
54
+ "$TMP_ROOT"/pi-agent-vent-*-npm-cache-*) ;;
55
+ *)
56
+ echo "Release command refused non-isolated npm cache: $cache" >&2
57
+ return 1
58
+ ;;
59
+ esac
60
+ if [[ "$prefix" != "-" ]]; then
61
+ case "$prefix" in
62
+ "$TMP_ROOT"/pi-agent-vent-*-npm-prefix-*) ;;
63
+ *)
64
+ echo "Release command refused non-isolated npm prefix: $prefix" >&2
65
+ return 1
66
+ ;;
67
+ esac
68
+ (
69
+ export NPM_CONFIG_PREFIX="$prefix" NPM_CONFIG_CACHE="$cache"
70
+ export NPM_CONFIG_MIN_RELEASE_AGE="$RELEASE_MIN_AGE"
71
+ "$@"
72
+ )
73
+ else
74
+ (
75
+ export NPM_CONFIG_CACHE="$cache" NPM_CONFIG_MIN_RELEASE_AGE="$RELEASE_MIN_AGE"
76
+ "$@"
77
+ )
78
+ fi
79
+ }
80
+
81
+ release_npm_install() {
82
+ local cache="$1"
83
+ local prefix="$2"
84
+ shift 2
85
+ with_release_npm_policy "$cache" "$prefix" npm install \
86
+ --include=optional --ignore-scripts --no-audit --fund=false "$@"
87
+ }
88
+
89
+ CONTROL_NPM_CACHE="$(release_tmp_dir control-npm-cache)"
90
+ TEST_AGENT_DIR=""
91
+ TEST_NPM_PREFIX=""
92
+ TEST_NPM_CACHE=""
93
+ ARTIFACT_NPM_PREFIX=""
94
+ ARTIFACT_NPM_CACHE=""
95
+ ARTIFACT_TOOL_VENT_DIR=""
96
+ TARBALL_CHECK_DIR=""
97
+ TARBALL_NPM_CACHE=""
98
+ TARBALL_PATH=""
99
+ cleanup() {
100
+ if [[ "${KEEP_RELEASE_ARTIFACTS:-0}" != "1" ]]; then
101
+ for path_to_remove in "$CONTROL_NPM_CACHE" "$TEST_AGENT_DIR" "$TEST_NPM_PREFIX" \
102
+ "$TEST_NPM_CACHE" "$ARTIFACT_NPM_PREFIX" "$ARTIFACT_NPM_CACHE" \
103
+ "$ARTIFACT_TOOL_VENT_DIR" "$TARBALL_CHECK_DIR" "$TARBALL_NPM_CACHE"; do
104
+ if [[ -n "$path_to_remove" && -d "$path_to_remove" ]]; then
105
+ rm -rf "$path_to_remove"
106
+ fi
107
+ done
108
+ if [[ -n "$TARBALL_PATH" && -f "$TARBALL_PATH" ]]; then
109
+ rm -f "$TARBALL_PATH"
110
+ fi
111
+ fi
112
+ }
113
+ trap cleanup EXIT
114
+
11
115
  echo "== release-check: ${NAME}@${VERSION}"
116
+ node ./scripts/release-smoke-check.mjs assert-exact-host-contract \
117
+ --package-json package.json \
118
+ --host-version "$HOST_VERSION"
12
119
 
13
120
  if [[ -z "$REPOSITORY_URL" ]]; then
14
121
  echo "package.json repository.url is required for provenance release publishing." >&2
@@ -21,14 +128,17 @@ if [[ "$NAME" != "${NAME,,}" ]]; then
21
128
  fi
22
129
 
23
130
  echo "== npm pack --dry-run --json"
24
- PACK_JSON="$(npm pack --dry-run --json)"
131
+ PACK_JSON="$(npm --cache "$CONTROL_NPM_CACHE" pack --dry-run --json)"
25
132
  echo "$PACK_JSON"
133
+ if [[ -f "$REPO_ROOT/scripts/npm-pack-json.mjs" ]]; then
134
+ PACK_JSON="$(printf '%s' "$PACK_JSON" | node "$REPO_ROOT/scripts/npm-pack-json.mjs")"
135
+ fi
26
136
 
27
137
  PACK_JSON="$PACK_JSON" node ./scripts/release-artifact-check.mjs
28
138
 
29
139
  echo "== npm publish --dry-run"
30
140
  set +e
31
- PUBLISH_DRY_RUN_OUTPUT="$(npm publish --dry-run 2>&1)"
141
+ PUBLISH_DRY_RUN_OUTPUT="$(npm --cache "$CONTROL_NPM_CACHE" publish --dry-run 2>&1)"
32
142
  PUBLISH_DRY_RUN_EXIT=$?
33
143
  set -e
34
144
  echo "$PUBLISH_DRY_RUN_OUTPUT"
@@ -41,61 +151,26 @@ if [[ "$PUBLISH_DRY_RUN_EXIT" -ne 0 ]]; then
41
151
  fi
42
152
  fi
43
153
 
44
- TEST_AGENT_DIR=""
45
- TEST_NPM_PREFIX=""
46
- TEST_NPM_CACHE=""
47
- ARTIFACT_NPM_PREFIX=""
48
- ARTIFACT_NPM_CACHE=""
49
- ARTIFACT_TOOL_VENT_DIR=""
50
- TARBALL_CHECK_DIR=""
51
- TARBALL_PATH=""
52
- cleanup() {
53
- if [[ "${KEEP_RELEASE_ARTIFACTS:-0}" != "1" ]]; then
54
- if [[ -n "$TEST_AGENT_DIR" && -d "$TEST_AGENT_DIR" ]]; then
55
- rm -rf "$TEST_AGENT_DIR"
56
- fi
57
- if [[ -n "$TEST_NPM_PREFIX" && -d "$TEST_NPM_PREFIX" ]]; then
58
- rm -rf "$TEST_NPM_PREFIX"
59
- fi
60
- if [[ -n "$TEST_NPM_CACHE" && -d "$TEST_NPM_CACHE" ]]; then
61
- rm -rf "$TEST_NPM_CACHE"
62
- fi
63
- if [[ -n "$ARTIFACT_NPM_PREFIX" && -d "$ARTIFACT_NPM_PREFIX" ]]; then
64
- rm -rf "$ARTIFACT_NPM_PREFIX"
65
- fi
66
- if [[ -n "$ARTIFACT_NPM_CACHE" && -d "$ARTIFACT_NPM_CACHE" ]]; then
67
- rm -rf "$ARTIFACT_NPM_CACHE"
68
- fi
69
- if [[ -n "$ARTIFACT_TOOL_VENT_DIR" && -d "$ARTIFACT_TOOL_VENT_DIR" ]]; then
70
- rm -rf "$ARTIFACT_TOOL_VENT_DIR"
71
- fi
72
- if [[ -n "$TARBALL_CHECK_DIR" && -d "$TARBALL_CHECK_DIR" ]]; then
73
- rm -rf "$TARBALL_CHECK_DIR"
74
- fi
75
- if [[ -n "$TARBALL_PATH" && -f "$TARBALL_PATH" ]]; then
76
- rm -f "$TARBALL_PATH"
77
- fi
78
- fi
79
- }
80
- trap cleanup EXIT
81
-
82
154
  echo "== npm pack"
83
- TARBALL="$(npm pack --silent | tail -n 1)"
155
+ TARBALL="$(npm --cache "$CONTROL_NPM_CACHE" pack --silent | tail -n 1)"
84
156
  TARBALL_PATH="$ROOT_DIR/$TARBALL"
85
157
  echo "Tarball: $TARBALL_PATH"
86
158
 
87
- TARBALL_CHECK_DIR="$(mktemp -d /tmp/pi-agent-vent-tarball-check-XXXXXX)"
159
+ TARBALL_CHECK_DIR="$(release_tmp_dir tarball-check)"
160
+ TARBALL_NPM_CACHE="$(release_tmp_dir tarball-npm-cache)"
88
161
  echo "== unpacked tarball package contract"
89
162
  tar -xzf "$TARBALL_PATH" -C "$TARBALL_CHECK_DIR"
90
163
  (
91
164
  cd "$TARBALL_CHECK_DIR/package"
92
- npm install --ignore-scripts --no-audit --fund=false
165
+ # This isolated artifact probe intentionally selects the exact host contract above.
166
+ # Ordinary installs retain the workstation's npm release-age policy.
167
+ release_npm_install "$TARBALL_NPM_CACHE" -
93
168
  npm run check
94
169
  )
95
170
 
96
- ARTIFACT_NPM_PREFIX="$(mktemp -d /tmp/pi-agent-vent-artifact-npm-prefix-XXXXXX)"
97
- ARTIFACT_NPM_CACHE="$(mktemp -d /tmp/pi-agent-vent-artifact-npm-cache-XXXXXX)"
98
- ARTIFACT_TOOL_VENT_DIR="$(mktemp -d /tmp/pi-agent-vent-artifact-tool-store-XXXXXX)"
171
+ ARTIFACT_NPM_PREFIX="$(release_tmp_dir artifact-npm-prefix)"
172
+ ARTIFACT_NPM_CACHE="$(release_tmp_dir artifact-npm-cache)"
173
+ ARTIFACT_TOOL_VENT_DIR="$(release_tmp_dir artifact-tool-store)"
99
174
  ARTIFACT_PACKAGE_ROOT="$(npm --prefix "$ARTIFACT_NPM_PREFIX" root -g)/$NAME"
100
175
 
101
176
  case "$ARTIFACT_PACKAGE_ROOT" in
@@ -107,8 +182,8 @@ case "$ARTIFACT_PACKAGE_ROOT" in
107
182
  esac
108
183
 
109
184
  echo "== npm installed artifact shadow registered-tool smoke (no Pi auth)"
110
- npm --prefix "$ARTIFACT_NPM_PREFIX" --cache "$ARTIFACT_NPM_CACHE" \
111
- install --global --ignore-scripts --no-audit --fund=false "$TARBALL_PATH"
185
+ release_npm_install "$ARTIFACT_NPM_CACHE" "$ARTIFACT_NPM_PREFIX" \
186
+ --prefix "$ARTIFACT_NPM_PREFIX" --global "$TARBALL_PATH"
112
187
  node ./scripts/release-smoke-check.mjs assert-installed-artifact \
113
188
  --package-root "$ARTIFACT_PACKAGE_ROOT" \
114
189
  --package-name "$NAME" \
@@ -124,15 +199,19 @@ else
124
199
  echo "pi CLI not found in PATH." >&2
125
200
  exit 1
126
201
  fi
202
+ INSTALLED_PI_VERSION="$(pi --version)"
203
+ node ./scripts/release-smoke-check.mjs assert-exact-host-contract \
204
+ --package-json package.json \
205
+ --host-version "$INSTALLED_PI_VERSION"
127
206
  if [[ ! -f "$HOME/.pi/agent/auth.json" ]]; then
128
207
  echo "Missing $HOME/.pi/agent/auth.json (needed for isolated pi smoke tests)." >&2
129
208
  echo "Tip: set SKIP_PI_SMOKE=1 for artifact-only checks." >&2
130
209
  exit 1
131
210
  fi
132
211
 
133
- TEST_AGENT_DIR="$(mktemp -d /tmp/pi-extension-release-check-XXXXXX)"
134
- TEST_NPM_PREFIX="$(mktemp -d /tmp/pi-extension-release-npm-prefix-XXXXXX)"
135
- TEST_NPM_CACHE="$(mktemp -d /tmp/pi-extension-release-npm-cache-XXXXXX)"
212
+ TEST_AGENT_DIR="$(release_tmp_dir pi-agent-dir)"
213
+ TEST_NPM_PREFIX="$(release_tmp_dir pi-npm-prefix)"
214
+ TEST_NPM_CACHE="$(release_tmp_dir pi-npm-cache)"
136
215
 
137
216
  cp "$HOME/.pi/agent/auth.json" "$TEST_AGENT_DIR/auth.json"
138
217
 
@@ -153,8 +232,8 @@ JSON
153
232
 
154
233
  echo "== pi install tarball (isolated PI_CODING_AGENT_DIR and npm prefix)"
155
234
  PACKAGE_SPEC="npm:$TARBALL_PATH"
156
- NPM_CONFIG_PREFIX="$TEST_NPM_PREFIX" NPM_CONFIG_CACHE="$TEST_NPM_CACHE" \
157
- PI_CODING_AGENT_DIR="$TEST_AGENT_DIR" pi install "$PACKAGE_SPEC"
235
+ PI_CODING_AGENT_DIR="$TEST_AGENT_DIR" \
236
+ with_release_npm_policy "$TEST_NPM_CACHE" "$TEST_NPM_PREFIX" pi install "$PACKAGE_SPEC"
158
237
 
159
238
  echo "== verify tarball package recorded in settings"
160
239
  TEST_AGENT_DIR="$TEST_AGENT_DIR" PACKAGE_SPEC="$PACKAGE_SPEC" node <<'NODE'
@@ -178,14 +257,16 @@ NODE
178
257
 
179
258
  if [[ -x "./scripts/release-smoke.sh" ]]; then
180
259
  echo "== extension-specific smoke checks (scripts/release-smoke.sh)"
260
+ PI_INSTALLED_PACKAGE_ROOT="$TEST_AGENT_DIR/npm/node_modules/$NAME"
181
261
  NPM_CONFIG_PREFIX="$TEST_NPM_PREFIX" NPM_CONFIG_CACHE="$TEST_NPM_CACHE" \
182
- PI_CODING_AGENT_DIR="$TEST_AGENT_DIR" PACKAGE_SPEC="$PACKAGE_SPEC" bash ./scripts/release-smoke.sh
262
+ PI_CODING_AGENT_DIR="$TEST_AGENT_DIR" PACKAGE_SPEC="$PACKAGE_SPEC" \
263
+ INSTALLED_PACKAGE_ROOT="$PI_INSTALLED_PACKAGE_ROOT" bash ./scripts/release-smoke.sh
183
264
  fi
184
265
  fi
185
266
 
186
267
  echo "== npm view ${NAME} version (pre-publish may be 404)"
187
268
  set +e
188
- npm view "$NAME" version --json --registry https://registry.npmjs.org/
269
+ npm --cache "$CONTROL_NPM_CACHE" view "$NAME" version --json --registry https://registry.npmjs.org/
189
270
  VIEW_EXIT=$?
190
271
  set -e
191
272
  echo "npm view exit: $VIEW_EXIT"
@@ -1,3 +1,9 @@
1
+ // ---
2
+ // summary: supplies assertions and isolated tool-path probes for release smoke testing
3
+ // read_when:
4
+ // - changing installed-artifact validation or agent_vent release smoke behavior
5
+ // ---
6
+
1
7
  import fs from "node:fs";
2
8
  import os from "node:os";
3
9
  import path from "node:path";
@@ -5,6 +11,22 @@ import { pathToFileURL } from "node:url";
5
11
 
6
12
  export const readJsonFile = (filePath) => JSON.parse(fs.readFileSync(filePath, "utf8"));
7
13
 
14
+ export const assertExactHostContract = ({ packageJson, hostVersion }) => {
15
+ const piAi = packageJson?.devDependencies?.["@earendil-works/pi-ai"];
16
+ const codingAgent = packageJson?.devDependencies?.["@earendil-works/pi-coding-agent"];
17
+ if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(hostVersion || "")) {
18
+ throw new Error(
19
+ `Release smoke requires an exact Pi host version, got: ${hostVersion || "(empty)"}`,
20
+ );
21
+ }
22
+ if (piAi !== hostVersion || codingAgent !== hostVersion) {
23
+ throw new Error(
24
+ `Release smoke host contract mismatch: pi=${hostVersion}, pi-ai=${piAi || "(missing)"}, pi-coding-agent=${codingAgent || "(missing)"}`,
25
+ );
26
+ }
27
+ return hostVersion;
28
+ };
29
+
8
30
  export const packageSourcesFromSettings = (settings) => {
9
31
  const packages = Array.isArray(settings?.packages) ? settings.packages : [];
10
32
  return packages
@@ -292,6 +314,15 @@ const readArgValue = (args, name) => {
292
314
  const runCli = async () => {
293
315
  const [command, ...args] = process.argv.slice(2);
294
316
 
317
+ if (command === "assert-exact-host-contract") {
318
+ const packageJsonPath = readArgValue(args, "--package-json");
319
+ const hostVersion = readArgValue(args, "--host-version");
320
+ if (!packageJsonPath) throw new Error("--package-json is required");
321
+ assertExactHostContract({ packageJson: readJsonFile(packageJsonPath), hostVersion });
322
+ console.log(`Exact Pi host contract selected: ${hostVersion}.`);
323
+ return;
324
+ }
325
+
295
326
  if (command === "assert-settings") {
296
327
  const settingsPath = readArgValue(args, "--settings");
297
328
  const packageSpec = readArgValue(args, "--package-spec");
@@ -353,7 +384,7 @@ const runCli = async () => {
353
384
  }
354
385
 
355
386
  throw new Error(
356
- "Usage: node ./scripts/release-smoke-check.mjs <assert-settings|assert-local-tarball-install-source|assert-installed-artifact|prepare-local-path-artifact-settings|assert-command-output|assert-installed-tool-path> ...",
387
+ "Usage: node ./scripts/release-smoke-check.mjs <assert-exact-host-contract|assert-settings|assert-local-tarball-install-source|assert-installed-artifact|prepare-local-path-artifact-settings|assert-command-output|assert-installed-tool-path> ...",
357
388
  );
358
389
  };
359
390
 
@@ -1,4 +1,9 @@
1
1
  #!/usr/bin/env bash
2
+ # ---
3
+ # summary: smoke-tests the installed agent_vent package through isolated pi discovery
4
+ # read_when:
5
+ # - verifying tarball installation, command output, or registered-tool execution
6
+ # ---
2
7
  set -euo pipefail
3
8
 
4
9
  ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
@@ -14,6 +19,11 @@ if [[ -z "${PI_CODING_AGENT_DIR:-}" ]]; then
14
19
  exit 1
15
20
  fi
16
21
 
22
+ if [[ -z "${INSTALLED_PACKAGE_ROOT:-}" ]]; then
23
+ echo "INSTALLED_PACKAGE_ROOT is required; the caller must provide Pi's isolated installed-artifact path." >&2
24
+ exit 1
25
+ fi
26
+
17
27
  if ! command -v pi >/dev/null 2>&1; then
18
28
  echo "pi CLI not found in PATH." >&2
19
29
  exit 1
@@ -38,23 +48,15 @@ SMOKE_TOOL_VENT_DIR="$SMOKE_DIR/agent-vent-tool-store"
38
48
  SMOKE_LOCAL_PATH_OUTPUT="$SMOKE_DIR/agent-vent-local-path.out"
39
49
  PACKAGE_NAME="$(node -p "JSON.parse(require('node:fs').readFileSync('package.json', 'utf8')).name")"
40
50
  PACKAGE_VERSION="$(node -p "JSON.parse(require('node:fs').readFileSync('package.json', 'utf8')).version")"
41
- if [[ -n "${NPM_CONFIG_PREFIX:-}" ]]; then
42
- GLOBAL_NODE_MODULES="$(npm --prefix "$NPM_CONFIG_PREFIX" root -g)"
43
- else
44
- GLOBAL_NODE_MODULES="$(npm root -g)"
45
- fi
46
- INSTALLED_PACKAGE_ROOT="$GLOBAL_NODE_MODULES/$PACKAGE_NAME"
47
51
  INSTALLED_EXTENSION_PATH="$INSTALLED_PACKAGE_ROOT/extensions/agent-vent.ts"
48
52
 
49
- if [[ -n "${NPM_CONFIG_PREFIX:-}" ]]; then
50
- case "$INSTALLED_PACKAGE_ROOT" in
51
- "$NPM_CONFIG_PREFIX"/*) ;;
52
- *)
53
- echo "Installed package root escaped isolated npm prefix: $INSTALLED_PACKAGE_ROOT" >&2
54
- exit 1
55
- ;;
56
- esac
57
- fi
53
+ case "$(realpath -m "$INSTALLED_PACKAGE_ROOT")" in
54
+ "$(realpath "$PI_CODING_AGENT_DIR")"/*) ;;
55
+ *)
56
+ echo "Installed package root escaped isolated Pi agent directory: $INSTALLED_PACKAGE_ROOT" >&2
57
+ exit 1
58
+ ;;
59
+ esac
58
60
 
59
61
  node ./scripts/release-smoke-check.mjs assert-settings \
60
62
  --settings "$PI_CODING_AGENT_DIR/settings.json" \
@@ -1,4 +1,9 @@
1
1
  #!/usr/bin/env node
2
+ // ---
3
+ // summary: enforces pi-agent-vent manifest, script, extension, and lane structure contracts
4
+ // read_when:
5
+ // - editing package metadata, copier answers, publish files, or required npm scripts
6
+ // ---
2
7
 
3
8
  import fs from "node:fs";
4
9
 
@@ -92,7 +97,7 @@ function validatePackageJson() {
92
97
  }
93
98
  }
94
99
 
95
- const requiredPeers = ["@mariozechner/pi-coding-agent", "@mariozechner/pi-ai"];
100
+ const requiredPeers = ["@earendil-works/pi-coding-agent", "@earendil-works/pi-ai"];
96
101
  for (const peer of requiredPeers) {
97
102
  if (typeof p.peerDependencies?.[peer] !== "string") {
98
103
  fail(`package.json peerDependencies must include ${peer}`);
@@ -129,8 +134,8 @@ function validatePackageJson() {
129
134
  fail("package.json publishConfig.access must be 'public'");
130
135
  }
131
136
 
132
- if (p.engines?.node !== ">=22") {
133
- fail("package.json engines.node must be '>=22'");
137
+ if (p.engines?.node !== ">=22.19.0") {
138
+ fail("package.json engines.node must be '>=22.19.0'");
134
139
  }
135
140
 
136
141
  const expectedWorkspacePath = readCopierAnswer("workspace_relative_path");
@@ -1,4 +1,9 @@
1
1
  #!/usr/bin/env bash
2
+ # ---
3
+ # summary: verifies the pi-agent-vent scaffold tree, executables, and markdown frontmatter
4
+ # read_when:
5
+ # - checking filesystem conformance after package scaffold or documentation changes
6
+ # ---
2
7
  set -euo pipefail
3
8
 
4
9
  ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
@@ -33,8 +38,8 @@ required_files=(
33
38
  "scripts/validate-structure.sh"
34
39
  "scripts/validate-structure.mjs"
35
40
  "scripts/quality-gate.sh"
36
- "prompts/implementation-planning.md"
37
- "prompts/security-review.md"
41
+ "prompts/pi-agent-vent-implementation-planning.md"
42
+ "prompts/pi-agent-vent-security-review.md"
38
43
  )
39
44
 
40
45
  required_dirs=(
package/src/vent-store.js CHANGED
@@ -1,3 +1,9 @@
1
+ // ---
2
+ // summary: implements private agent vent storage, review projections, and retention safeguards
3
+ // read_when:
4
+ // - changing diagnostic record schemas, redaction, curation, review, export, or retention logic
5
+ // ---
6
+
1
7
  import { createHash, randomUUID } from "node:crypto";
2
8
  import fs from "node:fs";
3
9
  import os from "node:os";
@@ -22,6 +28,19 @@ export const CATEGORIES = [
22
28
  "workflow",
23
29
  "other",
24
30
  ];
31
+ export const CATEGORY_ALIASES = Object.freeze({
32
+ workflow_friction: "workflow",
33
+ operator_friction: "workflow",
34
+ process_friction: "workflow",
35
+ missing_affordance: "missing_capability",
36
+ missing_feature: "missing_capability",
37
+ documentation_gap: "documentation",
38
+ docs_gap: "documentation",
39
+ context_window: "context_loss",
40
+ context_friction: "context_loss",
41
+ context_alignment: "other",
42
+ tooling_friction: "friction",
43
+ });
25
44
  export const SEVERITIES = ["low", "medium", "high", "critical"];
26
45
  export const REVIEW_STATES = ["new", "acknowledged", "dismissed", "escalation_drafted"];
27
46
  export const CURATION_ACTIONS = ["merge", "rename", "remove"];
@@ -71,12 +90,22 @@ export function defaultBackupDir(env = process.env) {
71
90
  return path.join(defaultStoreDir(env), BACKUP_DIR_NAME);
72
91
  }
73
92
 
74
- export function normalizeCategory(value) {
75
- const normalized = String(value || "other")
93
+ export function normalizeCategoryToken(value) {
94
+ return String(value || "")
76
95
  .trim()
77
96
  .toLowerCase()
78
97
  .replaceAll("-", "_");
79
- return CATEGORIES.includes(normalized) ? normalized : "other";
98
+ }
99
+
100
+ export function resolveCategoryFilter(value) {
101
+ const normalized = normalizeCategoryToken(value);
102
+ if (!normalized) return undefined;
103
+ const candidate = CATEGORY_ALIASES[normalized] || normalized;
104
+ return CATEGORIES.includes(candidate) ? candidate : undefined;
105
+ }
106
+
107
+ export function normalizeCategory(value) {
108
+ return resolveCategoryFilter(value || "other") || "other";
80
109
  }
81
110
 
82
111
  export function normalizeSeverity(value) {
@@ -227,6 +256,56 @@ function collectRedactionMetadata(fields) {
227
256
  return { redacted, redactionPatterns: [...redactionPatterns].sort() };
228
257
  }
229
258
 
259
+ function hasDiagnosticAnchor(input = {}) {
260
+ return Boolean(
261
+ compactText(input?.frustration, 1200) ||
262
+ compactText(input?.evidence, 1600) ||
263
+ compactText(input?.expected, 800) ||
264
+ compactText(input?.actual, 800) ||
265
+ compactText(input?.reproduction, 1200) ||
266
+ compactText(input?.tool || input?.toolName, 160) ||
267
+ compactText(input?.packageName, 200) ||
268
+ compactText(input?.recurrenceKey, 200) ||
269
+ (Array.isArray(input?.tags) && input.tags.some((tag) => compactText(tag, 120))),
270
+ );
271
+ }
272
+
273
+ export function assessVentRecordQuality(input = {}, record = undefined) {
274
+ const summary = compactText(input?.summary ?? record?.summary, 600) || "";
275
+ const normalizedSummary = summary.toLowerCase();
276
+ const wordCount = normalizedSummary.split(/\s+/).filter(Boolean).length;
277
+ const anchored = hasDiagnosticAnchor(input);
278
+ const issues = [];
279
+ const warnings = [];
280
+
281
+ if (
282
+ /^(done|ok|okay|fixed|test|testing|progress|update|note|misc|bad|ugh)$/.test(normalizedSummary)
283
+ ) {
284
+ issues.push("summary is too generic for a diagnostic record");
285
+ }
286
+ if (summary.length < 12 && !anchored) {
287
+ issues.push("summary is too short without a diagnostic anchor");
288
+ }
289
+ if (wordCount < 4 && !anchored) {
290
+ warnings.push(
291
+ "add evidence, expected/actual, tool, package, tag, or recurrenceKey before recording",
292
+ );
293
+ }
294
+ if (normalizeCategory(input?.category ?? record?.category) === "other" && !anchored) {
295
+ warnings.push(
296
+ "category defaults to other; add a specific category or local diagnostic facet if known",
297
+ );
298
+ }
299
+
300
+ return {
301
+ recordable: issues.length === 0,
302
+ issues,
303
+ warnings,
304
+ boundary:
305
+ "Quality check is local anti-junk guidance only; it does not create AK evidence, tasks, issues, incidents, telemetry, owner routing, or ASC/self state.",
306
+ };
307
+ }
308
+
230
309
  export function createVentRecord(input, context = {}) {
231
310
  const summary = sanitizeOptionalText(input?.summary, 600);
232
311
  if (!summary.value) {
@@ -2588,13 +2667,13 @@ function assertNoEmptyReviewFilterValues(input = {}) {
2588
2667
 
2589
2668
  function normalizeReviewFilterCategory(value) {
2590
2669
  if (value === undefined) return undefined;
2591
- const normalized = String(value).trim().toLowerCase().replaceAll("-", "_");
2592
- if (!CATEGORIES.includes(normalized)) {
2670
+ const category = resolveCategoryFilter(value);
2671
+ if (!category) {
2593
2672
  throw new Error(
2594
2673
  `invalid agent_vent review filter category: ${value}; expected one of ${CATEGORIES.join(", ")}`,
2595
2674
  );
2596
2675
  }
2597
- return normalized;
2676
+ return category;
2598
2677
  }
2599
2678
 
2600
2679
  function sanitizeReviewFilterTags(value) {