@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.
- package/README.md +10 -3
- package/docs/engineering.local.md +13 -0
- package/docs/project/2026-05-21-agent-vent-design.md +7 -5
- package/docs/project/2026-06-05-self-toolbox-agent-vent-diagnostic-boundary.md +69 -0
- package/docs/project/product-posture.md +2 -0
- package/docs/project/vision.md +4 -0
- package/extensions/agent-vent.ts +95 -13
- package/package.json +16 -7
- package/policy/engineering-lane.json +13 -1
- package/prompts/{implementation-planning.md → pi-agent-vent-implementation-planning.md} +3 -3
- package/prompts/{security-review.md → pi-agent-vent-security-review.md} +3 -3
- package/scripts/docs-list.sh +5 -0
- package/scripts/quality-gate.sh +5 -0
- package/scripts/release-artifact-check.mjs +35 -5
- package/scripts/release-check.sh +136 -55
- package/scripts/release-smoke-check.mjs +32 -1
- package/scripts/release-smoke.sh +17 -15
- package/scripts/validate-structure.mjs +8 -3
- package/scripts/validate-structure.sh +7 -2
- package/src/vent-store.js +85 -6
- package/tests/agent-vent-extension.test.js +190 -1
- package/tests/release-artifact-check.test.js +69 -2
- package/tests/release-smoke-check.test.js +25 -0
- package/tests/vent-store.test.js +43 -0
|
@@ -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
|
-
|
|
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
|
|
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
|
|
186
|
-
const
|
|
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) {
|
package/scripts/release-check.sh
CHANGED
|
@@ -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="$(
|
|
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
|
-
|
|
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="$(
|
|
97
|
-
ARTIFACT_NPM_CACHE="$(
|
|
98
|
-
ARTIFACT_TOOL_VENT_DIR="$(
|
|
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
|
-
|
|
111
|
-
|
|
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="$(
|
|
134
|
-
TEST_NPM_PREFIX="$(
|
|
135
|
-
TEST_NPM_CACHE="$(
|
|
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
|
-
|
|
157
|
-
|
|
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"
|
|
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
|
|
package/scripts/release-smoke.sh
CHANGED
|
@@ -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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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 = ["@
|
|
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
|
|
75
|
-
|
|
93
|
+
export function normalizeCategoryToken(value) {
|
|
94
|
+
return String(value || "")
|
|
76
95
|
.trim()
|
|
77
96
|
.toLowerCase()
|
|
78
97
|
.replaceAll("-", "_");
|
|
79
|
-
|
|
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
|
|
2592
|
-
if (!
|
|
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
|
|
2676
|
+
return category;
|
|
2598
2677
|
}
|
|
2599
2678
|
|
|
2600
2679
|
function sanitizeReviewFilterTags(value) {
|