@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.
@@ -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
- npm install --ignore-scripts --no-audit --fund=false
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
- npm --prefix "$ARTIFACT_NPM_PREFIX" --cache "$ARTIFACT_NPM_CACHE" \
111
- install --global --ignore-scripts --no-audit --fund=false "$TARBALL_PATH"
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-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)"
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
- NPM_CONFIG_PREFIX="$TEST_NPM_PREFIX" NPM_CONFIG_CACHE="$TEST_NPM_CACHE" \
157
- PI_CODING_AGENT_DIR="$TEST_AGENT_DIR" pi install "$PACKAGE_SPEC"
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" bash ./scripts/release-smoke.sh
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
 
@@ -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
- 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
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 = ["@mariozechner/pi-coding-agent", "@mariozechner/pi-ai"];
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 normalizeCategory(value) {
75
- const normalized = String(value || "other")
86
+ export function normalizeCategoryToken(value) {
87
+ return String(value || "")
76
88
  .trim()
77
89
  .toLowerCase()
78
90
  .replaceAll("-", "_");
79
- return CATEGORIES.includes(normalized) ? normalized : "other";
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 normalized = String(value).trim().toLowerCase().replaceAll("-", "_");
2592
- if (!CATEGORIES.includes(normalized)) {
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 normalized;
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);