@tryinget/pi-agent-vent 0.1.1 → 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.
- 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 +66 -0
- package/docs/project/product-posture.md +3 -1
- package/docs/project/vision.md +4 -0
- package/extensions/agent-vent.ts +93 -14
- 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/release-check.sh +96 -51
- package/scripts/release-smoke-check.mjs +26 -1
- package/scripts/release-smoke.sh +12 -15
- package/scripts/validate-structure.mjs +3 -3
- package/scripts/validate-structure.sh +2 -2
- package/src/vent-store.js +78 -6
- package/tests/agent-vent-extension.test.js +166 -1
- package/tests/release-smoke-check.test.js +19 -0
- package/tests/vent-store.test.js +24 -0
package/scripts/release-check.sh
CHANGED
|
@@ -8,7 +8,81 @@ NAME="$(node -p "JSON.parse(require('node:fs').readFileSync('package.json', 'utf
|
|
|
8
8
|
VERSION="$(node -p "JSON.parse(require('node:fs').readFileSync('package.json', 'utf8')).version")"
|
|
9
9
|
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
10
|
|
|
11
|
+
HOST_VERSION="$(node -p "JSON.parse(require('node:fs').readFileSync('package.json', 'utf8')).devDependencies['@earendil-works/pi-coding-agent'] || ''")"
|
|
12
|
+
|
|
13
|
+
# One release-only policy membrane. Every dependency-resolving command, including
|
|
14
|
+
# Pi's nested npm invocation, must enter through here with disposable cache/prefix.
|
|
15
|
+
RELEASE_MIN_AGE=0
|
|
16
|
+
with_release_npm_policy() {
|
|
17
|
+
local cache="$1"
|
|
18
|
+
local prefix="$2"
|
|
19
|
+
shift 2
|
|
20
|
+
case "$cache" in
|
|
21
|
+
/tmp/pi-agent-vent-*-npm-cache-*) ;;
|
|
22
|
+
*)
|
|
23
|
+
echo "Release command refused non-isolated npm cache: $cache" >&2
|
|
24
|
+
return 1
|
|
25
|
+
;;
|
|
26
|
+
esac
|
|
27
|
+
if [[ "$prefix" != "-" ]]; then
|
|
28
|
+
case "$prefix" in
|
|
29
|
+
/tmp/pi-agent-vent-*-npm-prefix-*) ;;
|
|
30
|
+
*)
|
|
31
|
+
echo "Release command refused non-isolated npm prefix: $prefix" >&2
|
|
32
|
+
return 1
|
|
33
|
+
;;
|
|
34
|
+
esac
|
|
35
|
+
(
|
|
36
|
+
export NPM_CONFIG_PREFIX="$prefix" NPM_CONFIG_CACHE="$cache"
|
|
37
|
+
export NPM_CONFIG_MIN_RELEASE_AGE="$RELEASE_MIN_AGE"
|
|
38
|
+
"$@"
|
|
39
|
+
)
|
|
40
|
+
else
|
|
41
|
+
(
|
|
42
|
+
export NPM_CONFIG_CACHE="$cache" NPM_CONFIG_MIN_RELEASE_AGE="$RELEASE_MIN_AGE"
|
|
43
|
+
"$@"
|
|
44
|
+
)
|
|
45
|
+
fi
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
release_npm_install() {
|
|
49
|
+
local cache="$1"
|
|
50
|
+
local prefix="$2"
|
|
51
|
+
shift 2
|
|
52
|
+
with_release_npm_policy "$cache" "$prefix" npm install \
|
|
53
|
+
--ignore-scripts --no-audit --fund=false "$@"
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
CONTROL_NPM_CACHE="$(mktemp -d /tmp/pi-agent-vent-control-npm-cache-XXXXXX)"
|
|
57
|
+
TEST_AGENT_DIR=""
|
|
58
|
+
TEST_NPM_PREFIX=""
|
|
59
|
+
TEST_NPM_CACHE=""
|
|
60
|
+
ARTIFACT_NPM_PREFIX=""
|
|
61
|
+
ARTIFACT_NPM_CACHE=""
|
|
62
|
+
ARTIFACT_TOOL_VENT_DIR=""
|
|
63
|
+
TARBALL_CHECK_DIR=""
|
|
64
|
+
TARBALL_NPM_CACHE=""
|
|
65
|
+
TARBALL_PATH=""
|
|
66
|
+
cleanup() {
|
|
67
|
+
if [[ "${KEEP_RELEASE_ARTIFACTS:-0}" != "1" ]]; then
|
|
68
|
+
for path_to_remove in "$CONTROL_NPM_CACHE" "$TEST_AGENT_DIR" "$TEST_NPM_PREFIX" \
|
|
69
|
+
"$TEST_NPM_CACHE" "$ARTIFACT_NPM_PREFIX" "$ARTIFACT_NPM_CACHE" \
|
|
70
|
+
"$ARTIFACT_TOOL_VENT_DIR" "$TARBALL_CHECK_DIR" "$TARBALL_NPM_CACHE"; do
|
|
71
|
+
if [[ -n "$path_to_remove" && -d "$path_to_remove" ]]; then
|
|
72
|
+
rm -rf "$path_to_remove"
|
|
73
|
+
fi
|
|
74
|
+
done
|
|
75
|
+
if [[ -n "$TARBALL_PATH" && -f "$TARBALL_PATH" ]]; then
|
|
76
|
+
rm -f "$TARBALL_PATH"
|
|
77
|
+
fi
|
|
78
|
+
fi
|
|
79
|
+
}
|
|
80
|
+
trap cleanup EXIT
|
|
81
|
+
|
|
11
82
|
echo "== release-check: ${NAME}@${VERSION}"
|
|
83
|
+
node ./scripts/release-smoke-check.mjs assert-exact-host-contract \
|
|
84
|
+
--package-json package.json \
|
|
85
|
+
--host-version "$HOST_VERSION"
|
|
12
86
|
|
|
13
87
|
if [[ -z "$REPOSITORY_URL" ]]; then
|
|
14
88
|
echo "package.json repository.url is required for provenance release publishing." >&2
|
|
@@ -21,14 +95,14 @@ if [[ "$NAME" != "${NAME,,}" ]]; then
|
|
|
21
95
|
fi
|
|
22
96
|
|
|
23
97
|
echo "== npm pack --dry-run --json"
|
|
24
|
-
PACK_JSON="$(npm pack --dry-run --json)"
|
|
98
|
+
PACK_JSON="$(npm --cache "$CONTROL_NPM_CACHE" pack --dry-run --json)"
|
|
25
99
|
echo "$PACK_JSON"
|
|
26
100
|
|
|
27
101
|
PACK_JSON="$PACK_JSON" node ./scripts/release-artifact-check.mjs
|
|
28
102
|
|
|
29
103
|
echo "== npm publish --dry-run"
|
|
30
104
|
set +e
|
|
31
|
-
PUBLISH_DRY_RUN_OUTPUT="$(npm publish --dry-run 2>&1)"
|
|
105
|
+
PUBLISH_DRY_RUN_OUTPUT="$(npm --cache "$CONTROL_NPM_CACHE" publish --dry-run 2>&1)"
|
|
32
106
|
PUBLISH_DRY_RUN_EXIT=$?
|
|
33
107
|
set -e
|
|
34
108
|
echo "$PUBLISH_DRY_RUN_OUTPUT"
|
|
@@ -41,55 +115,20 @@ if [[ "$PUBLISH_DRY_RUN_EXIT" -ne 0 ]]; then
|
|
|
41
115
|
fi
|
|
42
116
|
fi
|
|
43
117
|
|
|
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
118
|
echo "== npm pack"
|
|
83
|
-
TARBALL="$(npm pack --silent | tail -n 1)"
|
|
119
|
+
TARBALL="$(npm --cache "$CONTROL_NPM_CACHE" pack --silent | tail -n 1)"
|
|
84
120
|
TARBALL_PATH="$ROOT_DIR/$TARBALL"
|
|
85
121
|
echo "Tarball: $TARBALL_PATH"
|
|
86
122
|
|
|
87
123
|
TARBALL_CHECK_DIR="$(mktemp -d /tmp/pi-agent-vent-tarball-check-XXXXXX)"
|
|
124
|
+
TARBALL_NPM_CACHE="$(mktemp -d /tmp/pi-agent-vent-tarball-npm-cache-XXXXXX)"
|
|
88
125
|
echo "== unpacked tarball package contract"
|
|
89
126
|
tar -xzf "$TARBALL_PATH" -C "$TARBALL_CHECK_DIR"
|
|
90
127
|
(
|
|
91
128
|
cd "$TARBALL_CHECK_DIR/package"
|
|
92
|
-
|
|
129
|
+
# This isolated artifact probe intentionally selects the exact host contract above.
|
|
130
|
+
# Ordinary installs retain the workstation's npm release-age policy.
|
|
131
|
+
release_npm_install "$TARBALL_NPM_CACHE" -
|
|
93
132
|
npm run check
|
|
94
133
|
)
|
|
95
134
|
|
|
@@ -107,8 +146,8 @@ case "$ARTIFACT_PACKAGE_ROOT" in
|
|
|
107
146
|
esac
|
|
108
147
|
|
|
109
148
|
echo "== npm installed artifact shadow registered-tool smoke (no Pi auth)"
|
|
110
|
-
|
|
111
|
-
|
|
149
|
+
release_npm_install "$ARTIFACT_NPM_CACHE" "$ARTIFACT_NPM_PREFIX" \
|
|
150
|
+
--prefix "$ARTIFACT_NPM_PREFIX" --global "$TARBALL_PATH"
|
|
112
151
|
node ./scripts/release-smoke-check.mjs assert-installed-artifact \
|
|
113
152
|
--package-root "$ARTIFACT_PACKAGE_ROOT" \
|
|
114
153
|
--package-name "$NAME" \
|
|
@@ -124,15 +163,19 @@ else
|
|
|
124
163
|
echo "pi CLI not found in PATH." >&2
|
|
125
164
|
exit 1
|
|
126
165
|
fi
|
|
166
|
+
INSTALLED_PI_VERSION="$(pi --version)"
|
|
167
|
+
node ./scripts/release-smoke-check.mjs assert-exact-host-contract \
|
|
168
|
+
--package-json package.json \
|
|
169
|
+
--host-version "$INSTALLED_PI_VERSION"
|
|
127
170
|
if [[ ! -f "$HOME/.pi/agent/auth.json" ]]; then
|
|
128
171
|
echo "Missing $HOME/.pi/agent/auth.json (needed for isolated pi smoke tests)." >&2
|
|
129
172
|
echo "Tip: set SKIP_PI_SMOKE=1 for artifact-only checks." >&2
|
|
130
173
|
exit 1
|
|
131
174
|
fi
|
|
132
175
|
|
|
133
|
-
TEST_AGENT_DIR="$(mktemp -d /tmp/pi-
|
|
134
|
-
TEST_NPM_PREFIX="$(mktemp -d /tmp/pi-
|
|
135
|
-
TEST_NPM_CACHE="$(mktemp -d /tmp/pi-
|
|
176
|
+
TEST_AGENT_DIR="$(mktemp -d /tmp/pi-agent-vent-pi-agent-dir-XXXXXX)"
|
|
177
|
+
TEST_NPM_PREFIX="$(mktemp -d /tmp/pi-agent-vent-pi-npm-prefix-XXXXXX)"
|
|
178
|
+
TEST_NPM_CACHE="$(mktemp -d /tmp/pi-agent-vent-pi-npm-cache-XXXXXX)"
|
|
136
179
|
|
|
137
180
|
cp "$HOME/.pi/agent/auth.json" "$TEST_AGENT_DIR/auth.json"
|
|
138
181
|
|
|
@@ -153,8 +196,8 @@ JSON
|
|
|
153
196
|
|
|
154
197
|
echo "== pi install tarball (isolated PI_CODING_AGENT_DIR and npm prefix)"
|
|
155
198
|
PACKAGE_SPEC="npm:$TARBALL_PATH"
|
|
156
|
-
|
|
157
|
-
|
|
199
|
+
PI_CODING_AGENT_DIR="$TEST_AGENT_DIR" \
|
|
200
|
+
with_release_npm_policy "$TEST_NPM_CACHE" "$TEST_NPM_PREFIX" pi install "$PACKAGE_SPEC"
|
|
158
201
|
|
|
159
202
|
echo "== verify tarball package recorded in settings"
|
|
160
203
|
TEST_AGENT_DIR="$TEST_AGENT_DIR" PACKAGE_SPEC="$PACKAGE_SPEC" node <<'NODE'
|
|
@@ -178,14 +221,16 @@ NODE
|
|
|
178
221
|
|
|
179
222
|
if [[ -x "./scripts/release-smoke.sh" ]]; then
|
|
180
223
|
echo "== extension-specific smoke checks (scripts/release-smoke.sh)"
|
|
224
|
+
PI_INSTALLED_PACKAGE_ROOT="$TEST_AGENT_DIR/npm/node_modules/$NAME"
|
|
181
225
|
NPM_CONFIG_PREFIX="$TEST_NPM_PREFIX" NPM_CONFIG_CACHE="$TEST_NPM_CACHE" \
|
|
182
|
-
PI_CODING_AGENT_DIR="$TEST_AGENT_DIR" PACKAGE_SPEC="$PACKAGE_SPEC"
|
|
226
|
+
PI_CODING_AGENT_DIR="$TEST_AGENT_DIR" PACKAGE_SPEC="$PACKAGE_SPEC" \
|
|
227
|
+
INSTALLED_PACKAGE_ROOT="$PI_INSTALLED_PACKAGE_ROOT" bash ./scripts/release-smoke.sh
|
|
183
228
|
fi
|
|
184
229
|
fi
|
|
185
230
|
|
|
186
231
|
echo "== npm view ${NAME} version (pre-publish may be 404)"
|
|
187
232
|
set +e
|
|
188
|
-
npm view "$NAME" version --json --registry https://registry.npmjs.org/
|
|
233
|
+
npm --cache "$CONTROL_NPM_CACHE" view "$NAME" version --json --registry https://registry.npmjs.org/
|
|
189
234
|
VIEW_EXIT=$?
|
|
190
235
|
set -e
|
|
191
236
|
echo "npm view exit: $VIEW_EXIT"
|
|
@@ -5,6 +5,22 @@ import { pathToFileURL } from "node:url";
|
|
|
5
5
|
|
|
6
6
|
export const readJsonFile = (filePath) => JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
7
7
|
|
|
8
|
+
export const assertExactHostContract = ({ packageJson, hostVersion }) => {
|
|
9
|
+
const piAi = packageJson?.devDependencies?.["@earendil-works/pi-ai"];
|
|
10
|
+
const codingAgent = packageJson?.devDependencies?.["@earendil-works/pi-coding-agent"];
|
|
11
|
+
if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(hostVersion || "")) {
|
|
12
|
+
throw new Error(
|
|
13
|
+
`Release smoke requires an exact Pi host version, got: ${hostVersion || "(empty)"}`,
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
if (piAi !== hostVersion || codingAgent !== hostVersion) {
|
|
17
|
+
throw new Error(
|
|
18
|
+
`Release smoke host contract mismatch: pi=${hostVersion}, pi-ai=${piAi || "(missing)"}, pi-coding-agent=${codingAgent || "(missing)"}`,
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
return hostVersion;
|
|
22
|
+
};
|
|
23
|
+
|
|
8
24
|
export const packageSourcesFromSettings = (settings) => {
|
|
9
25
|
const packages = Array.isArray(settings?.packages) ? settings.packages : [];
|
|
10
26
|
return packages
|
|
@@ -292,6 +308,15 @@ const readArgValue = (args, name) => {
|
|
|
292
308
|
const runCli = async () => {
|
|
293
309
|
const [command, ...args] = process.argv.slice(2);
|
|
294
310
|
|
|
311
|
+
if (command === "assert-exact-host-contract") {
|
|
312
|
+
const packageJsonPath = readArgValue(args, "--package-json");
|
|
313
|
+
const hostVersion = readArgValue(args, "--host-version");
|
|
314
|
+
if (!packageJsonPath) throw new Error("--package-json is required");
|
|
315
|
+
assertExactHostContract({ packageJson: readJsonFile(packageJsonPath), hostVersion });
|
|
316
|
+
console.log(`Exact Pi host contract selected: ${hostVersion}.`);
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
|
|
295
320
|
if (command === "assert-settings") {
|
|
296
321
|
const settingsPath = readArgValue(args, "--settings");
|
|
297
322
|
const packageSpec = readArgValue(args, "--package-spec");
|
|
@@ -353,7 +378,7 @@ const runCli = async () => {
|
|
|
353
378
|
}
|
|
354
379
|
|
|
355
380
|
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> ...",
|
|
381
|
+
"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
382
|
);
|
|
358
383
|
};
|
|
359
384
|
|
package/scripts/release-smoke.sh
CHANGED
|
@@ -14,6 +14,11 @@ if [[ -z "${PI_CODING_AGENT_DIR:-}" ]]; then
|
|
|
14
14
|
exit 1
|
|
15
15
|
fi
|
|
16
16
|
|
|
17
|
+
if [[ -z "${INSTALLED_PACKAGE_ROOT:-}" ]]; then
|
|
18
|
+
echo "INSTALLED_PACKAGE_ROOT is required; the caller must provide Pi's isolated installed-artifact path." >&2
|
|
19
|
+
exit 1
|
|
20
|
+
fi
|
|
21
|
+
|
|
17
22
|
if ! command -v pi >/dev/null 2>&1; then
|
|
18
23
|
echo "pi CLI not found in PATH." >&2
|
|
19
24
|
exit 1
|
|
@@ -38,23 +43,15 @@ SMOKE_TOOL_VENT_DIR="$SMOKE_DIR/agent-vent-tool-store"
|
|
|
38
43
|
SMOKE_LOCAL_PATH_OUTPUT="$SMOKE_DIR/agent-vent-local-path.out"
|
|
39
44
|
PACKAGE_NAME="$(node -p "JSON.parse(require('node:fs').readFileSync('package.json', 'utf8')).name")"
|
|
40
45
|
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
46
|
INSTALLED_EXTENSION_PATH="$INSTALLED_PACKAGE_ROOT/extensions/agent-vent.ts"
|
|
48
47
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
esac
|
|
57
|
-
fi
|
|
48
|
+
case "$(realpath -m "$INSTALLED_PACKAGE_ROOT")" in
|
|
49
|
+
"$(realpath "$PI_CODING_AGENT_DIR")"/*) ;;
|
|
50
|
+
*)
|
|
51
|
+
echo "Installed package root escaped isolated Pi agent directory: $INSTALLED_PACKAGE_ROOT" >&2
|
|
52
|
+
exit 1
|
|
53
|
+
;;
|
|
54
|
+
esac
|
|
58
55
|
|
|
59
56
|
node ./scripts/release-smoke-check.mjs assert-settings \
|
|
60
57
|
--settings "$PI_CODING_AGENT_DIR/settings.json" \
|
|
@@ -92,7 +92,7 @@ function validatePackageJson() {
|
|
|
92
92
|
}
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
const requiredPeers = ["@
|
|
95
|
+
const requiredPeers = ["@earendil-works/pi-coding-agent", "@earendil-works/pi-ai"];
|
|
96
96
|
for (const peer of requiredPeers) {
|
|
97
97
|
if (typeof p.peerDependencies?.[peer] !== "string") {
|
|
98
98
|
fail(`package.json peerDependencies must include ${peer}`);
|
|
@@ -129,8 +129,8 @@ function validatePackageJson() {
|
|
|
129
129
|
fail("package.json publishConfig.access must be 'public'");
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
-
if (p.engines?.node !== ">=22") {
|
|
133
|
-
fail("package.json engines.node must be '>=22'");
|
|
132
|
+
if (p.engines?.node !== ">=22.19.0") {
|
|
133
|
+
fail("package.json engines.node must be '>=22.19.0'");
|
|
134
134
|
}
|
|
135
135
|
|
|
136
136
|
const expectedWorkspacePath = readCopierAnswer("workspace_relative_path");
|
|
@@ -33,8 +33,8 @@ required_files=(
|
|
|
33
33
|
"scripts/validate-structure.sh"
|
|
34
34
|
"scripts/validate-structure.mjs"
|
|
35
35
|
"scripts/quality-gate.sh"
|
|
36
|
-
"prompts/implementation-planning.md"
|
|
37
|
-
"prompts/security-review.md"
|
|
36
|
+
"prompts/pi-agent-vent-implementation-planning.md"
|
|
37
|
+
"prompts/pi-agent-vent-security-review.md"
|
|
38
38
|
)
|
|
39
39
|
|
|
40
40
|
required_dirs=(
|
package/src/vent-store.js
CHANGED
|
@@ -22,6 +22,18 @@ export const CATEGORIES = [
|
|
|
22
22
|
"workflow",
|
|
23
23
|
"other",
|
|
24
24
|
];
|
|
25
|
+
export const CATEGORY_ALIASES = Object.freeze({
|
|
26
|
+
workflow_friction: "workflow",
|
|
27
|
+
operator_friction: "workflow",
|
|
28
|
+
process_friction: "workflow",
|
|
29
|
+
missing_affordance: "missing_capability",
|
|
30
|
+
missing_feature: "missing_capability",
|
|
31
|
+
documentation_gap: "documentation",
|
|
32
|
+
docs_gap: "documentation",
|
|
33
|
+
context_window: "context_loss",
|
|
34
|
+
context_friction: "context_loss",
|
|
35
|
+
tooling_friction: "friction",
|
|
36
|
+
});
|
|
25
37
|
export const SEVERITIES = ["low", "medium", "high", "critical"];
|
|
26
38
|
export const REVIEW_STATES = ["new", "acknowledged", "dismissed", "escalation_drafted"];
|
|
27
39
|
export const CURATION_ACTIONS = ["merge", "rename", "remove"];
|
|
@@ -71,12 +83,22 @@ export function defaultBackupDir(env = process.env) {
|
|
|
71
83
|
return path.join(defaultStoreDir(env), BACKUP_DIR_NAME);
|
|
72
84
|
}
|
|
73
85
|
|
|
74
|
-
export function
|
|
75
|
-
|
|
86
|
+
export function normalizeCategoryToken(value) {
|
|
87
|
+
return String(value || "")
|
|
76
88
|
.trim()
|
|
77
89
|
.toLowerCase()
|
|
78
90
|
.replaceAll("-", "_");
|
|
79
|
-
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function resolveCategoryFilter(value) {
|
|
94
|
+
const normalized = normalizeCategoryToken(value);
|
|
95
|
+
if (!normalized) return undefined;
|
|
96
|
+
const candidate = CATEGORY_ALIASES[normalized] || normalized;
|
|
97
|
+
return CATEGORIES.includes(candidate) ? candidate : undefined;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function normalizeCategory(value) {
|
|
101
|
+
return resolveCategoryFilter(value || "other") || "other";
|
|
80
102
|
}
|
|
81
103
|
|
|
82
104
|
export function normalizeSeverity(value) {
|
|
@@ -227,6 +249,56 @@ function collectRedactionMetadata(fields) {
|
|
|
227
249
|
return { redacted, redactionPatterns: [...redactionPatterns].sort() };
|
|
228
250
|
}
|
|
229
251
|
|
|
252
|
+
function hasDiagnosticAnchor(input = {}) {
|
|
253
|
+
return Boolean(
|
|
254
|
+
compactText(input?.frustration, 1200) ||
|
|
255
|
+
compactText(input?.evidence, 1600) ||
|
|
256
|
+
compactText(input?.expected, 800) ||
|
|
257
|
+
compactText(input?.actual, 800) ||
|
|
258
|
+
compactText(input?.reproduction, 1200) ||
|
|
259
|
+
compactText(input?.tool || input?.toolName, 160) ||
|
|
260
|
+
compactText(input?.packageName, 200) ||
|
|
261
|
+
compactText(input?.recurrenceKey, 200) ||
|
|
262
|
+
(Array.isArray(input?.tags) && input.tags.some((tag) => compactText(tag, 120))),
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export function assessVentRecordQuality(input = {}, record = undefined) {
|
|
267
|
+
const summary = compactText(input?.summary ?? record?.summary, 600) || "";
|
|
268
|
+
const normalizedSummary = summary.toLowerCase();
|
|
269
|
+
const wordCount = normalizedSummary.split(/\s+/).filter(Boolean).length;
|
|
270
|
+
const anchored = hasDiagnosticAnchor(input);
|
|
271
|
+
const issues = [];
|
|
272
|
+
const warnings = [];
|
|
273
|
+
|
|
274
|
+
if (
|
|
275
|
+
/^(done|ok|okay|fixed|test|testing|progress|update|note|misc|bad|ugh)$/.test(normalizedSummary)
|
|
276
|
+
) {
|
|
277
|
+
issues.push("summary is too generic for a diagnostic record");
|
|
278
|
+
}
|
|
279
|
+
if (summary.length < 12 && !anchored) {
|
|
280
|
+
issues.push("summary is too short without a diagnostic anchor");
|
|
281
|
+
}
|
|
282
|
+
if (wordCount < 4 && !anchored) {
|
|
283
|
+
warnings.push(
|
|
284
|
+
"add evidence, expected/actual, tool, package, tag, or recurrenceKey before recording",
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
if (normalizeCategory(input?.category ?? record?.category) === "other" && !anchored) {
|
|
288
|
+
warnings.push(
|
|
289
|
+
"category defaults to other; add a specific category or local diagnostic facet if known",
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return {
|
|
294
|
+
recordable: issues.length === 0,
|
|
295
|
+
issues,
|
|
296
|
+
warnings,
|
|
297
|
+
boundary:
|
|
298
|
+
"Quality check is local anti-junk guidance only; it does not create AK evidence, tasks, issues, incidents, telemetry, owner routing, or ASC/self state.",
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
230
302
|
export function createVentRecord(input, context = {}) {
|
|
231
303
|
const summary = sanitizeOptionalText(input?.summary, 600);
|
|
232
304
|
if (!summary.value) {
|
|
@@ -2588,13 +2660,13 @@ function assertNoEmptyReviewFilterValues(input = {}) {
|
|
|
2588
2660
|
|
|
2589
2661
|
function normalizeReviewFilterCategory(value) {
|
|
2590
2662
|
if (value === undefined) return undefined;
|
|
2591
|
-
const
|
|
2592
|
-
if (!
|
|
2663
|
+
const category = resolveCategoryFilter(value);
|
|
2664
|
+
if (!category) {
|
|
2593
2665
|
throw new Error(
|
|
2594
2666
|
`invalid agent_vent review filter category: ${value}; expected one of ${CATEGORIES.join(", ")}`,
|
|
2595
2667
|
);
|
|
2596
2668
|
}
|
|
2597
|
-
return
|
|
2669
|
+
return category;
|
|
2598
2670
|
}
|
|
2599
2671
|
|
|
2600
2672
|
function sanitizeReviewFilterTags(value) {
|
|
@@ -3,15 +3,17 @@ import fs from "node:fs";
|
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import test from "node:test";
|
|
6
|
-
import agentVentExtension from "../extensions/agent-vent.ts";
|
|
6
|
+
import agentVentExtension, { _test } from "../extensions/agent-vent.ts";
|
|
7
7
|
import { createCurationEvent, createVentRecord, readReviewEvents } from "../src/vent-store.js";
|
|
8
8
|
|
|
9
9
|
function createMockPi() {
|
|
10
10
|
const tools = new Map();
|
|
11
11
|
const commands = new Map();
|
|
12
|
+
const messages = [];
|
|
12
13
|
return {
|
|
13
14
|
tools,
|
|
14
15
|
commands,
|
|
16
|
+
messages,
|
|
15
17
|
api: {
|
|
16
18
|
registerTool(tool) {
|
|
17
19
|
tools.set(tool.name, tool);
|
|
@@ -19,6 +21,9 @@ function createMockPi() {
|
|
|
19
21
|
registerCommand(name, command) {
|
|
20
22
|
commands.set(name, command);
|
|
21
23
|
},
|
|
24
|
+
sendMessage(message) {
|
|
25
|
+
messages.push(message);
|
|
26
|
+
},
|
|
22
27
|
},
|
|
23
28
|
};
|
|
24
29
|
}
|
|
@@ -39,6 +44,8 @@ test("agent_vent tool schema stays aligned with retention candidate and compare
|
|
|
39
44
|
const schemaText = JSON.stringify(pi.tools.get("agent_vent").parameters);
|
|
40
45
|
|
|
41
46
|
assert.match(schemaText, /"compare"/);
|
|
47
|
+
assert.match(schemaText, /"preview"/);
|
|
48
|
+
assert.match(schemaText, /"workflow_friction"/);
|
|
42
49
|
assert.match(schemaText, /"candidates"/);
|
|
43
50
|
assert.match(schemaText, /"history"/);
|
|
44
51
|
assert.match(schemaText, /retentionCandidateState/);
|
|
@@ -48,6 +55,41 @@ test("agent_vent tool schema stays aligned with retention candidate and compare
|
|
|
48
55
|
assert.match(schemaText, /outcomes, compare, export, or retention planning/);
|
|
49
56
|
});
|
|
50
57
|
|
|
58
|
+
test("agent_vent keeps long command output persistent while short statuses stay notifications", async () => {
|
|
59
|
+
const pi = createMockPi();
|
|
60
|
+
agentVentExtension(pi.api);
|
|
61
|
+
const notifications = [];
|
|
62
|
+
const ctx = { hasUI: true, ui: { notify: (message) => notifications.push(message) } };
|
|
63
|
+
|
|
64
|
+
await pi.commands.get("agent_vent").handler("help", ctx);
|
|
65
|
+
assert.equal(pi.messages.length, 1);
|
|
66
|
+
assert.equal(pi.messages[0].customType, "agent-vent-command");
|
|
67
|
+
assert.match(pi.messages[0].content, /agent_vent commands/);
|
|
68
|
+
assert.match(notifications[0], /added to the session transcript/);
|
|
69
|
+
|
|
70
|
+
await pi.commands.get("agent_vent").handler("unknown", ctx);
|
|
71
|
+
assert.equal(pi.messages.length, 1);
|
|
72
|
+
assert.match(notifications[1], /Unknown \/agent_vent action/);
|
|
73
|
+
assert.equal(_test.isLongCommandOutput("short"), false);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("agent_vent cancellation fails before expensive store work", async () => {
|
|
77
|
+
const pi = createMockPi();
|
|
78
|
+
agentVentExtension(pi.api);
|
|
79
|
+
const controller = new AbortController();
|
|
80
|
+
controller.abort();
|
|
81
|
+
await assert.rejects(
|
|
82
|
+
() =>
|
|
83
|
+
pi.tools
|
|
84
|
+
.get("agent_vent")
|
|
85
|
+
.execute("cancelled", { action: "summary" }, controller.signal, undefined, {
|
|
86
|
+
cwd: "/repo",
|
|
87
|
+
sessionManager: { getSessionFile: () => undefined },
|
|
88
|
+
}),
|
|
89
|
+
/cancelled/,
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
|
|
51
93
|
test("agent_vent records minimized local diagnostics without external authority claims", async () => {
|
|
52
94
|
const pi = createMockPi();
|
|
53
95
|
agentVentExtension(pi.api);
|
|
@@ -474,6 +516,129 @@ test("agent_vent records minimized local diagnostics without external authority
|
|
|
474
516
|
}
|
|
475
517
|
});
|
|
476
518
|
|
|
519
|
+
test("agent_vent accepts category aliases for tool records and command filters", async () => {
|
|
520
|
+
const pi = createMockPi();
|
|
521
|
+
agentVentExtension(pi.api);
|
|
522
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "agent-vent-alias-"));
|
|
523
|
+
const oldDir = process.env.PI_AGENT_VENT_DIR;
|
|
524
|
+
const oldLog = console.log;
|
|
525
|
+
const messages = [];
|
|
526
|
+
process.env.PI_AGENT_VENT_DIR = dir;
|
|
527
|
+
console.log = (message) => messages.push(String(message));
|
|
528
|
+
try {
|
|
529
|
+
const tool = pi.tools.get("agent_vent");
|
|
530
|
+
const result = await tool.execute(
|
|
531
|
+
"tool-call-category-alias",
|
|
532
|
+
{
|
|
533
|
+
action: "record",
|
|
534
|
+
summary: "Workflow friction alias should normalize",
|
|
535
|
+
category: "workflow_friction",
|
|
536
|
+
severity: "medium",
|
|
537
|
+
},
|
|
538
|
+
undefined,
|
|
539
|
+
undefined,
|
|
540
|
+
{
|
|
541
|
+
cwd: "/repo",
|
|
542
|
+
sessionManager: { getSessionFile: () => undefined },
|
|
543
|
+
},
|
|
544
|
+
);
|
|
545
|
+
|
|
546
|
+
assert.equal(result.details.record.category, "workflow");
|
|
547
|
+
assert.equal(
|
|
548
|
+
result.details.record.recurrenceKey,
|
|
549
|
+
"workflow:workflow-friction-alias-should-normalize",
|
|
550
|
+
);
|
|
551
|
+
|
|
552
|
+
const reviewResult = await tool.execute(
|
|
553
|
+
"tool-call-category-alias-review",
|
|
554
|
+
{ action: "review", category: "workflow_friction" },
|
|
555
|
+
undefined,
|
|
556
|
+
undefined,
|
|
557
|
+
{
|
|
558
|
+
cwd: "/repo",
|
|
559
|
+
sessionManager: { getSessionFile: () => undefined },
|
|
560
|
+
},
|
|
561
|
+
);
|
|
562
|
+
assert.equal(reviewResult.details.reviewQueue.filters.category, "workflow");
|
|
563
|
+
assert.equal(reviewResult.details.reviewQueue.matchingGroupCount, 1);
|
|
564
|
+
|
|
565
|
+
await pi.commands.get("agent_vent").handler("review category=workflow_friction", {
|
|
566
|
+
hasUI: false,
|
|
567
|
+
});
|
|
568
|
+
assert.match(messages[0], /Filters: category=workflow/);
|
|
569
|
+
assert.doesNotMatch(messages[0], /category=workflow_friction/);
|
|
570
|
+
} finally {
|
|
571
|
+
console.log = oldLog;
|
|
572
|
+
if (oldDir === undefined) delete process.env.PI_AGENT_VENT_DIR;
|
|
573
|
+
else process.env.PI_AGENT_VENT_DIR = oldDir;
|
|
574
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
575
|
+
}
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
test("agent_vent preview runs anti-junk checks without writing records", async () => {
|
|
579
|
+
const pi = createMockPi();
|
|
580
|
+
agentVentExtension(pi.api);
|
|
581
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "agent-vent-preview-"));
|
|
582
|
+
const oldDir = process.env.PI_AGENT_VENT_DIR;
|
|
583
|
+
process.env.PI_AGENT_VENT_DIR = dir;
|
|
584
|
+
try {
|
|
585
|
+
const tool = pi.tools.get("agent_vent");
|
|
586
|
+
const preview = await tool.execute(
|
|
587
|
+
"tool-call-preview",
|
|
588
|
+
{
|
|
589
|
+
action: "preview",
|
|
590
|
+
summary: "Workflow friction alias should normalize before recording",
|
|
591
|
+
category: "workflow_friction",
|
|
592
|
+
tool: "self",
|
|
593
|
+
},
|
|
594
|
+
undefined,
|
|
595
|
+
undefined,
|
|
596
|
+
{
|
|
597
|
+
cwd: "/repo",
|
|
598
|
+
sessionManager: { getSessionFile: () => undefined },
|
|
599
|
+
},
|
|
600
|
+
);
|
|
601
|
+
|
|
602
|
+
assert.equal(preview.details.wouldRecord, true);
|
|
603
|
+
assert.equal(preview.details.recordPreview.category, "workflow");
|
|
604
|
+
assert.match(preview.content[0].text, /No local diagnostic record was written/);
|
|
605
|
+
assert.equal(fs.existsSync(path.join(dir, "vents.jsonl")), false);
|
|
606
|
+
|
|
607
|
+
const junkPreview = await tool.execute(
|
|
608
|
+
"tool-call-preview-junk",
|
|
609
|
+
{ action: "preview", summary: "done" },
|
|
610
|
+
undefined,
|
|
611
|
+
undefined,
|
|
612
|
+
{
|
|
613
|
+
cwd: "/repo",
|
|
614
|
+
sessionManager: { getSessionFile: () => undefined },
|
|
615
|
+
},
|
|
616
|
+
);
|
|
617
|
+
assert.equal(junkPreview.details.wouldRecord, false);
|
|
618
|
+
assert.match(junkPreview.content[0].text, /not recordable/);
|
|
619
|
+
|
|
620
|
+
await assert.rejects(
|
|
621
|
+
() =>
|
|
622
|
+
tool.execute(
|
|
623
|
+
"tool-call-record-junk",
|
|
624
|
+
{ action: "record", summary: "done" },
|
|
625
|
+
undefined,
|
|
626
|
+
undefined,
|
|
627
|
+
{
|
|
628
|
+
cwd: "/repo",
|
|
629
|
+
sessionManager: { getSessionFile: () => undefined },
|
|
630
|
+
},
|
|
631
|
+
),
|
|
632
|
+
/anti-junk quality check/,
|
|
633
|
+
);
|
|
634
|
+
assert.equal(fs.existsSync(path.join(dir, "vents.jsonl")), false);
|
|
635
|
+
} finally {
|
|
636
|
+
if (oldDir === undefined) delete process.env.PI_AGENT_VENT_DIR;
|
|
637
|
+
else process.env.PI_AGENT_VENT_DIR = oldDir;
|
|
638
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
639
|
+
}
|
|
640
|
+
});
|
|
641
|
+
|
|
477
642
|
test("agent_vent command rejects unknown review filter keys without creating stores", async () => {
|
|
478
643
|
const pi = createMockPi();
|
|
479
644
|
agentVentExtension(pi.api);
|