@tryinget/pi-agent-vent 0.1.2 → 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 +2 -0
- package/docs/project/vision.md +4 -0
- package/extensions/agent-vent.ts +88 -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/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/README.md
CHANGED
|
@@ -45,11 +45,12 @@ Then in Pi:
|
|
|
45
45
|
|
|
46
46
|
## Tool behavior
|
|
47
47
|
|
|
48
|
-
`agent_vent` supports
|
|
48
|
+
`agent_vent` supports fifteen actions:
|
|
49
49
|
|
|
50
50
|
| Action | Purpose |
|
|
51
51
|
|---|---|
|
|
52
|
-
| `record` | Append a minimized vent record. Requires `summary
|
|
52
|
+
| `record` | Append a minimized vent record. Requires `summary` and must pass the local anti-junk quality check. |
|
|
53
|
+
| `preview` | Build the sanitized would-be record and anti-junk quality result without writing the local store. |
|
|
53
54
|
| `summary` | Show recurrence groups and advisory candidate incidents. |
|
|
54
55
|
| `list` | Show recent local records. |
|
|
55
56
|
| `path` | Show the store path and boundary contract. |
|
|
@@ -99,11 +100,14 @@ The runtime-facing name is intentionally singular: use `agent_vent` for the LLM
|
|
|
99
100
|
|
|
100
101
|
`pi-agent-vent` is a companion to `pi-autonomous-session-control`, not part of it. ASC/`self` remains the execution and operational-mirror owner; this package owns only local vent diagnostics.
|
|
101
102
|
|
|
103
|
+
For the cross-package handoff between ASC `self` diagnostic candidates, toolbox activation, and `agent_vent` preview/record writes, see [Self, toolbox, and agent_vent diagnostic boundary](docs/project/2026-06-05-self-toolbox-agent-vent-diagnostic-boundary.md).
|
|
104
|
+
|
|
102
105
|
When `pi-toolbox-discovery` is installed, it exposes the `agent_vent` bundle so agents can discover or activate the same-named `agent_vent` tool on demand:
|
|
103
106
|
|
|
104
107
|
```ts
|
|
105
108
|
toolbox({ action: "search", query: "vent" })
|
|
106
109
|
toolbox({ action: "activate", bundle: "agent_vent" })
|
|
110
|
+
agent_vent({ action: "preview", summary: "...", category: "workflow", tool: "..." })
|
|
107
111
|
```
|
|
108
112
|
|
|
109
113
|
The owner extension must still be installed/reloaded so the `agent_vent` tool is registered before toolbox can activate it.
|
|
@@ -157,6 +161,7 @@ Product docs:
|
|
|
157
161
|
- [Vision](docs/project/vision.md)
|
|
158
162
|
- [Product posture](docs/project/product-posture.md)
|
|
159
163
|
- [Agent vent design](docs/project/2026-05-21-agent-vent-design.md)
|
|
164
|
+
- [Self/toolbox/agent_vent diagnostic boundary](docs/project/2026-06-05-self-toolbox-agent-vent-diagnostic-boundary.md)
|
|
160
165
|
- [Implementation plan](docs/project/2026-05-21-agent-vent-implementation-plan.md)
|
|
161
166
|
|
|
162
167
|
## Package checks
|
|
@@ -168,7 +173,9 @@ npm install
|
|
|
168
173
|
npm run check
|
|
169
174
|
```
|
|
170
175
|
|
|
171
|
-
For release confidence, `npm run release:check` packs the package, verifies packaged docs/scripts, runs the packaged fallback gate, installs the tarball into an isolated npm prefix for a no-auth shadow registered-tool `agent_vent path` smoke, then installs the tarball with isolated Pi settings and npm prefix/cache, validates that local `npm:<tarball>` is being used only as the install source, and smokes the installed artifact through local-path Pi package discovery with `/agent_vent path`. Use `npm run release:check:quick` for artifact-only checks when live Pi smoke is not available; quick checks still include the no-auth installed shadow registered-tool smoke.
|
|
176
|
+
For release confidence, `npm run release:check` packs the package, verifies packaged docs/scripts, runs the packaged fallback gate, installs the tarball into an isolated npm prefix for a no-auth shadow registered-tool `agent_vent path` smoke, then installs the tarball with isolated Pi settings and npm prefix/cache, validates that local `npm:<tarball>` is being used only as the install source, and smokes the installed artifact through local-path Pi package discovery with `/agent_vent path`. Use `npm run release:check:quick` for artifact-only checks when live Pi smoke is not available; quick checks still include the no-auth installed shadow registered-tool smoke.
|
|
177
|
+
|
|
178
|
+
Release probes pin `pi-ai` and `pi-coding-agent` to one exact version and fail before installation if that contract diverges from the selected/installed Pi host. Only the isolated artifact/probe installs set `min-release-age=0`, allowing a newly selected exact host contract to be tested while ordinary installs continue to obey the workstation supply-chain cutoff. These smokes prove artifact/package-loading behavior only, not npm/GitHub publication or provenance.
|
|
172
179
|
|
|
173
180
|
Run from monorepo root through the canonical package gate:
|
|
174
181
|
|
|
@@ -55,3 +55,16 @@ Not selected by default:
|
|
|
55
55
|
- Authority: candidate incidents are recommendations only; do not create or imply AK/GitHub/incident mutations.
|
|
56
56
|
- Validation: `npm run check` from this package, or root `bash ./scripts/package-quality-gate.sh ci packages/pi-agent-vent`.
|
|
57
57
|
- Optional companions are intentionally not adopted for v0.1: no `fast-check`, Cucumber, Nunjucks, or ts-quality rollout.
|
|
58
|
+
|
|
59
|
+
## Repo loop validation
|
|
60
|
+
|
|
61
|
+
`@tryinget/pi-agent-vent` adopts `repo-loop-validation-v1` for package-local loop prompt dogfooding. The policy declaration is in `policy/engineering-lane.json`.
|
|
62
|
+
|
|
63
|
+
- `loop-doctor`: `npm run loop-doctor` (non-failing Node/npm/package/git diagnostics)
|
|
64
|
+
- `loop-verify-fast`: `npm run loop-verify-fast` (maps to `quality:pre-commit`)
|
|
65
|
+
- `loop-impact-plan`: `npm run loop-impact-plan` (coarse package impact note plus changed-file listing)
|
|
66
|
+
- `loop-impact-run`: `npm run loop-impact-run` (maps to `npm run check`)
|
|
67
|
+
- `loop-impact-wide`: `npm run loop-impact-wide` (explicit full package gate, also `npm run check`)
|
|
68
|
+
- `loop-landing-check`: `npm run loop-landing-check` (maps to `npm run check`)
|
|
69
|
+
|
|
70
|
+
These commands produce package-local evidence for orchestration prompts. They do not replace Pi runtime install/reload proof, release approval, or monorepo owner authority.
|
|
@@ -95,7 +95,8 @@ The tool prompt and runtime validation both bias toward minimal summaries:
|
|
|
95
95
|
|
|
96
96
|
Actions:
|
|
97
97
|
|
|
98
|
-
- `record` — append a vent record.
|
|
98
|
+
- `record` — append a vent record after the local anti-junk quality check accepts it.
|
|
99
|
+
- `preview` — build the sanitized would-be record and anti-junk quality result without writing the local store.
|
|
99
100
|
- `summary` — summarize recurrence groups and candidate incidents.
|
|
100
101
|
- `list` — show recent records.
|
|
101
102
|
- `path` — show local store path and data contract.
|
|
@@ -112,7 +113,8 @@ Actions:
|
|
|
112
113
|
|
|
113
114
|
Important behavior:
|
|
114
115
|
|
|
115
|
-
- `record` requires `summary`.
|
|
116
|
+
- `record` requires `summary` and rejects low-signal generic payloads such as `done`.
|
|
117
|
+
- `preview` returns `recordPreview`, `quality`, and `wouldRecord` without appending JSONL; use it before `record` when the candidate came from `self`, another tool, or a generic/friction-heavy moment.
|
|
116
118
|
- `severity` defaults to `medium`.
|
|
117
119
|
- `category` defaults to `other`.
|
|
118
120
|
- `recurrenceKey` may be supplied by the agent; otherwise it is derived from category + summary.
|
|
@@ -164,11 +166,11 @@ This heuristic intentionally errs toward surfacing review candidates, not assert
|
|
|
164
166
|
|
|
165
167
|
## Cross-package integration
|
|
166
168
|
|
|
167
|
-
`pi-agent-vent` remains separate from `pi-autonomous-session-control` by design:
|
|
169
|
+
`pi-agent-vent` remains separate from `pi-autonomous-session-control` by design. The detailed handoff is documented in [Self, toolbox, and agent_vent diagnostic boundary](2026-06-05-self-toolbox-agent-vent-diagnostic-boundary.md):
|
|
168
170
|
|
|
169
|
-
- ASC/`self` owns operational introspection, subagent/runtime control,
|
|
170
|
-
- `pi-agent-vent` owns local diagnostic vent records, redaction, recurrence grouping, and advisory candidate-incident heuristics.
|
|
171
|
+
- ASC/`self` owns operational introspection, subagent/runtime control, mirror-only handoff/progress summaries, and typed `self.diagnostic_candidate.v1` suggestions.
|
|
171
172
|
- `pi-toolbox-discovery` owns discovery/activation of the already-registered `agent_vent` tool through the same-named `agent_vent` bundle; `agent-vent` is not a runtime alias.
|
|
173
|
+
- `pi-agent-vent` owns local diagnostic vent records, preview quality checks, redaction, recurrence grouping, and advisory candidate-incident heuristics.
|
|
172
174
|
|
|
173
175
|
This keeps vent persistence from becoming hidden ASC state while still making the capability discoverable during autonomous work.
|
|
174
176
|
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
---
|
|
2
|
+
summary: "Boundary contract for self diagnostic candidates, toolbox activation, and agent_vent local diagnostic records."
|
|
3
|
+
read_when:
|
|
4
|
+
- "Changing ASC self diagnostic-review output."
|
|
5
|
+
- "Changing toolbox agent_vent bundle discovery or activation guidance."
|
|
6
|
+
- "Changing agent_vent preview/record behavior or diagnostic authority wording."
|
|
7
|
+
system4d:
|
|
8
|
+
container: "Cross-package diagnostic handoff boundary."
|
|
9
|
+
compass: "Make recurring friction visible without turning diagnostics into authority."
|
|
10
|
+
engine: "self mirrors candidate -> toolbox activates bundle -> agent_vent previews/records local diagnostics -> human decides owner escalation."
|
|
11
|
+
fog: "Diagnostic candidates and local records can be mistaken for tasks, evidence, incidents, telemetry, or ASC state."
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# Self, toolbox, and agent_vent diagnostic boundary
|
|
15
|
+
|
|
16
|
+
## Purpose
|
|
17
|
+
|
|
18
|
+
This note documents the intended handoff between three Pi extension surfaces:
|
|
19
|
+
|
|
20
|
+
1. `self` from `pi-autonomous-session-control` notices current-session friction and can return a typed `self.diagnostic_candidate.v1` payload.
|
|
21
|
+
2. `toolbox` from `pi-toolbox-discovery` can discover or activate the already-registered `agent_vent` tool on demand.
|
|
22
|
+
3. `agent_vent` from `pi-agent-vent` can preview or record minimized local diagnostic records in its append-only local store.
|
|
23
|
+
|
|
24
|
+
The chain is deliberately not an escalation pipeline. It is a low-cost local diagnostic path for repeated bugs, tool failures, workflow friction, context loss, missing affordances, and similar agent-experience problems.
|
|
25
|
+
|
|
26
|
+
## Ownership contract
|
|
27
|
+
|
|
28
|
+
| Surface | Owns | Does not own |
|
|
29
|
+
|---|---|---|
|
|
30
|
+
| `self` | Moment-level mirror of the current session; candidate diagnostic payloads; suggested next local actions. | Durable vent records, recurrence truth, AK evidence/tasks, GitHub issues, incidents, telemetry, or owner routing. |
|
|
31
|
+
| `toolbox` | Bundle discovery, risk posture, and active-tool-set changes for already-registered tools. | Registering missing owner tools, validating vent quality, storing diagnostics, or deciding escalation. |
|
|
32
|
+
| `agent_vent` | Local JSONL diagnostic records, preview quality checks, redaction, recurrence grouping, review state, curation, draft-only text, export, and retention lifecycle. | AK evidence/tasks, GitHub issues, real incidents, external telemetry, ASC/self state, publication, or owner-system lifecycle. |
|
|
33
|
+
|
|
34
|
+
## Safe handoff sequence
|
|
35
|
+
|
|
36
|
+
Use this sequence when a session notices recurring or high-friction behavior worth possible local memory:
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
self({ query: "What friction just happened?" })
|
|
40
|
+
toolbox({ action: "activate", bundle: "agent_vent" })
|
|
41
|
+
agent_vent({ action: "preview", summary: "...", category: "...", tool: "...", packageName: "..." })
|
|
42
|
+
agent_vent({ action: "record", summary: "...", category: "...", tool: "...", packageName: "..." })
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Rules:
|
|
46
|
+
|
|
47
|
+
- `self` may prefill or suggest an `agent_vent` payload, but it must not write the record internally.
|
|
48
|
+
- `toolbox` activation only makes the `agent_vent` tool callable; it does not create or preview a diagnostic record.
|
|
49
|
+
- `agent_vent action=preview` sanitizes the would-be record and runs the anti-junk quality check without writing the store.
|
|
50
|
+
- `agent_vent action=record` writes only if the local quality check accepts the payload.
|
|
51
|
+
- Generic low-signal summaries such as `done` are rejected for `record`; use `preview` to inspect issues and warnings before writing.
|
|
52
|
+
- Human/operator judgment still decides whether any recurrence should become an AK task, GitHub issue, incident review, evidence record, publication, or owner-surface handoff.
|
|
53
|
+
|
|
54
|
+
## Copy wording for boundaries
|
|
55
|
+
|
|
56
|
+
Recommended short wording:
|
|
57
|
+
|
|
58
|
+
> Diagnostic review is mirror/local only: `self` can propose a candidate, `toolbox` can activate the `agent_vent` capability, and `agent_vent` can preview or write local diagnostic memory. None of these actions create AK evidence/tasks, GitHub issues, incidents, external telemetry, publication, owner routing, or ASC/self state.
|
|
59
|
+
|
|
60
|
+
Use stronger wording when a durable local write happened:
|
|
61
|
+
|
|
62
|
+
> A local `agent_vent` record was written to the operator's Pi diagnostic store. This is recurrence memory for review, not canonical evidence, task truth, issue state, incident declaration, telemetry, publication, or owner-system mutation.
|
|
63
|
+
|
|
64
|
+
## Review and escalation posture
|
|
65
|
+
|
|
66
|
+
`agent_vent` recurrence groups and candidate incidents mean “worth human review,” not “incident declared.” Draft outputs are paste-ready text only. Exports are diagnostic projections only. If a human decides a local recurrence deserves owner action, use the owning surface directly and record evidence there according to that owner’s rules.
|
|
@@ -114,6 +114,8 @@ The highest-leverage product line is:
|
|
|
114
114
|
local vent capture -> operator review queue -> draft-only owner routing -> human-approved escalation
|
|
115
115
|
```
|
|
116
116
|
|
|
117
|
+
For visible self-evolution work, keep `agent_vent` as recurrence memory and review queue only. The DRY routing map lives in the root `docs/project/visible-self-evolution-spine.md` spine; orchestrator may project verified evidence, while AK/society owner surfaces retain durable authority.
|
|
118
|
+
|
|
117
119
|
Do not add automatic GitHub/AK/incident writers. The local facet summary, fail-closed facet-filtered local review queue, per-state review outcome follow-up, read-only cross-state review comparison, explicit local decision posture, filter-preserving supported follow-up commands, record/state-scoped facet export, export-local-follow-up guidance, read-only retention-candidate planning, read-only retention-history receipt projection, advisory human-review hints, quoted state-aware next-action guidance, bounded review-detail samples, curation-aware recurrence resolution, facet-aware draft-only routing, retention archive/restore, and privacy membrane now have package validation; remaining product depth should refine operator comprehension only where it does not broaden authority. Hard-delete beyond backup-backed archive is decided out of v0.1: permanent removal remains operator-owned filesystem/data-lifecycle control unless a future decision accepts a narrower purge design.
|
|
118
120
|
|
|
119
121
|
Current proof: `npm run check` passes with 80 package tests plus release dry-run, packaged Markdown link checks, an unpacked-tarball `npm install && npm run check` contract smoke that runs the packaged fallback gate from inside the artifact, an artifact-only no-auth installed shadow registered-tool `agent_vent path` no-store-read smoke, and a full installed-tarball local-path package-discovery `/agent_vent path` smoke using isolated `PI_CODING_AGENT_DIR`, isolated npm prefix/cache, and `PI_AGENT_VENT_DIR`; docs strict check passes, `git diff --check` passes, and historical dogfood covered curation resolution, export posture, and live `pi install` reload behavior. Validation now covers quoted rollback commands, complete retention token inputs, stale-token/stale-restore failures, path-escape/symlink backup failures, receipt-failure rollback, retention-history restore-candidate reconstruction without active-store reads, export follow-up command quoting without archive/restore tokens or owner-routing claims, review decision-posture projection without resolution/assignment/evidence/incident claims, curation-aware recurrence resolution for review state and record feedback, stale lock cleanup, backup restore, duplicate-id-safe retention archive selection, retention-candidate planning without archive tokens, retention-candidate and compare tool-schema/command-contract parity, filter-preserving supported compare follow-ups, record/state-scoped facet export without owner-routing claims, mixed-group export non-broadening, empty export filters failing closed before store reads, tag/facet privacy metadata recomputation, hostile legacy JSONL privacy recomputation, fail-closed review/outcome/compare/export/retention-candidate/history syntax before store reads including empty filters, invalid review category/state handling, explicit per-state outcome/compare limits, quoted legacy recurrence-key command round trips, tool-only maintainer-note hints, reference-style packaged Markdown links, fail-closed `files[]` wildcard handling, public artifact script viability, release metadata alignment with `.copier-answers.yml`, local `npm:<tarball>` install-source validation, isolated installed-command smoke output, local-path package-discovery loading from the installed artifact, shadow registered-tool execution from the installed artifact, and registered-tool `path` no-store-read/no-store-write behavior with symlinked active stores.
|
package/docs/project/vision.md
CHANGED
|
@@ -44,6 +44,10 @@ agent notices recurring friction
|
|
|
44
44
|
-> owner system receives a human-approved draft only when appropriate
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
+
For self-evolution loops, `agent_vent` is the recurrence-memory surface after ASC/self has produced a diagnostic candidate.
|
|
48
|
+
It is not the loop executor, evaluator, or escalation authority.
|
|
49
|
+
Use the root `docs/project/visible-self-evolution-spine.md` spine and the cross-package [self/toolbox/agent_vent diagnostic boundary](./2026-06-05-self-toolbox-agent-vent-diagnostic-boundary.md) for the DRY owner map.
|
|
50
|
+
|
|
47
51
|
Near-term product work should make the review step better rather than broadening authority. Useful next surfaces include:
|
|
48
52
|
|
|
49
53
|
- `/agent_vent review` for an operator-facing review queue;
|
package/extensions/agent-vent.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import type { ExtensionAPI } from "@
|
|
2
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { Type } from "typebox";
|
|
4
4
|
import {
|
|
5
5
|
appendCurationEvent,
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
appendVentRecord,
|
|
8
8
|
archiveRecurrenceGroup,
|
|
9
9
|
assertCanCurateRecurrence,
|
|
10
|
+
assessVentRecordQuality,
|
|
10
11
|
buildEscalationDraft,
|
|
11
12
|
buildFacetSummary,
|
|
12
13
|
buildLifecycleSnapshot,
|
|
@@ -49,6 +50,7 @@ import {
|
|
|
49
50
|
normalizeReviewState,
|
|
50
51
|
RETENTION_ACTIONS,
|
|
51
52
|
readRetentionEvents,
|
|
53
|
+
resolveCategoryFilter,
|
|
52
54
|
resolveRecurrenceGroup,
|
|
53
55
|
restoreRetentionBackup,
|
|
54
56
|
SEVERITIES,
|
|
@@ -59,6 +61,7 @@ import {
|
|
|
59
61
|
|
|
60
62
|
const ACTIONS = [
|
|
61
63
|
"record",
|
|
64
|
+
"preview",
|
|
62
65
|
"summary",
|
|
63
66
|
"list",
|
|
64
67
|
"path",
|
|
@@ -79,6 +82,19 @@ const REVIEW_STATES = Array.isArray(STORE_REVIEW_STATES)
|
|
|
79
82
|
? STORE_REVIEW_STATES
|
|
80
83
|
: FALLBACK_REVIEW_STATES;
|
|
81
84
|
const RETENTION_CANDIDATE_STATES = ["reviewed", "all", ...REVIEW_STATES] as const;
|
|
85
|
+
const CATEGORY_ALIAS_INPUTS = [
|
|
86
|
+
"workflow_friction",
|
|
87
|
+
"operator_friction",
|
|
88
|
+
"process_friction",
|
|
89
|
+
"missing_affordance",
|
|
90
|
+
"missing_feature",
|
|
91
|
+
"documentation_gap",
|
|
92
|
+
"docs_gap",
|
|
93
|
+
"context_window",
|
|
94
|
+
"context_friction",
|
|
95
|
+
"tooling_friction",
|
|
96
|
+
] as const;
|
|
97
|
+
const CATEGORY_INPUTS = [...CATEGORIES, ...CATEGORY_ALIAS_INPUTS] as const;
|
|
82
98
|
|
|
83
99
|
const AgentVentParams = Type.Object({
|
|
84
100
|
action: Type.Optional(
|
|
@@ -92,9 +108,10 @@ const AgentVentParams = Type.Object({
|
|
|
92
108
|
),
|
|
93
109
|
category: Type.Optional(
|
|
94
110
|
Type.Union(
|
|
95
|
-
|
|
111
|
+
CATEGORY_INPUTS.map((category) => Type.Literal(category)),
|
|
96
112
|
{
|
|
97
|
-
description:
|
|
113
|
+
description:
|
|
114
|
+
"Local category for the frustration pattern. Common aliases are accepted and normalized to canonical categories.",
|
|
98
115
|
},
|
|
99
116
|
),
|
|
100
117
|
),
|
|
@@ -235,11 +252,12 @@ export default function agentVentExtension(pi: ExtensionAPI) {
|
|
|
235
252
|
name: "agent_vent",
|
|
236
253
|
label: "Agent Vent",
|
|
237
254
|
description:
|
|
238
|
-
"Record, review, and inspect local agent frustration events so recurring bugs, workflow friction, and missing affordances become visible.",
|
|
255
|
+
"Record, preview, review, and inspect local agent frustration events so recurring bugs, workflow friction, and missing affordances become visible.",
|
|
239
256
|
promptSnippet:
|
|
240
|
-
"
|
|
257
|
+
"Preview or record minimized local frustration events and review recurring patterns without creating incidents, tasks, issues, evidence records, or telemetry.",
|
|
241
258
|
promptGuidelines: [
|
|
242
259
|
"Use agent_vent when you encounter recurring agent frustration, long-lived bugs, repeated tool/runtime failures, context-loss patterns, or missing affordances worth later human review.",
|
|
260
|
+
"Use action=preview before action=record when the diagnostic may be generic, low-signal, or copied from another tool; previews run the anti-junk quality check without writing the local store.",
|
|
243
261
|
"Use action=review to inspect the local recurrence review queue; include recurrenceKey to inspect bounded representative samples for one local group; optionally filter review by local category, tag, tool, or package facets; use action=set_review to mark a recurrence group as new, acknowledged, dismissed, or escalation_drafted.",
|
|
244
262
|
"Use action=outcomes for read-only post-review follow-up across local review-state buckets; outcome guidance is local diagnostic UX only, not owner routing or external completion.",
|
|
245
263
|
"Use action=compare for a read-only cross-state review comparison before export, retention planning, or draft-only handoff; comparison output emits no archive/restore tokens and mutates nothing.",
|
|
@@ -253,7 +271,8 @@ export default function agentVentExtension(pi: ExtensionAPI) {
|
|
|
253
271
|
"When calling agent_vent, summarize minimally and never include secrets, credentials, private user payloads, or long raw logs.",
|
|
254
272
|
],
|
|
255
273
|
parameters: AgentVentParams,
|
|
256
|
-
async execute(_toolCallId, params,
|
|
274
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
275
|
+
throwIfCancelled(signal);
|
|
257
276
|
const storePath = defaultStorePath();
|
|
258
277
|
const reviewPath = defaultReviewPath();
|
|
259
278
|
const curationPath = defaultCurationPath();
|
|
@@ -275,13 +294,29 @@ export default function agentVentExtension(pi: ExtensionAPI) {
|
|
|
275
294
|
);
|
|
276
295
|
}
|
|
277
296
|
|
|
278
|
-
if (action === "record") {
|
|
297
|
+
if (action === "preview" || action === "record") {
|
|
279
298
|
const sessionFile = ctx.sessionManager.getSessionFile();
|
|
280
299
|
const record = createVentRecord(params, {
|
|
281
300
|
cwd: ctx.cwd,
|
|
282
301
|
sessionFile: sessionFile ? path.basename(sessionFile) : undefined,
|
|
283
|
-
source: "agent_vent_tool",
|
|
302
|
+
source: action === "preview" ? "agent_vent_preview" : "agent_vent_tool",
|
|
284
303
|
});
|
|
304
|
+
const quality = assessVentRecordQuality(params, record);
|
|
305
|
+
if (action === "preview") {
|
|
306
|
+
return textResult(formatRecordPreview(record, quality), {
|
|
307
|
+
action,
|
|
308
|
+
storePath,
|
|
309
|
+
recordPreview: record,
|
|
310
|
+
quality,
|
|
311
|
+
wouldRecord: quality.recordable,
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
if (!quality.recordable) {
|
|
315
|
+
throw new Error(
|
|
316
|
+
`agent_vent record rejected by anti-junk quality check: ${quality.issues.join("; ")}`,
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
throwIfCancelled(signal);
|
|
285
320
|
appendVentRecord(storePath, record);
|
|
286
321
|
const state = loadDiagnosticState({
|
|
287
322
|
storePath,
|
|
@@ -331,6 +366,7 @@ export default function agentVentExtension(pi: ExtensionAPI) {
|
|
|
331
366
|
});
|
|
332
367
|
}
|
|
333
368
|
|
|
369
|
+
throwIfCancelled(signal);
|
|
334
370
|
const state = loadDiagnosticState({
|
|
335
371
|
storePath,
|
|
336
372
|
reviewPath,
|
|
@@ -367,6 +403,7 @@ export default function agentVentExtension(pi: ExtensionAPI) {
|
|
|
367
403
|
};
|
|
368
404
|
assertCanCurateRecurrence(records, curationEvents, input);
|
|
369
405
|
const event = createCurationEvent(input, { source: "agent_vent_tool" });
|
|
406
|
+
throwIfCancelled(signal);
|
|
370
407
|
appendCurationEvent(curationPath, event);
|
|
371
408
|
const targetText = event.targetRecurrenceKey ? ` -> ${event.targetRecurrenceKey}` : "";
|
|
372
409
|
const text = [
|
|
@@ -400,6 +437,7 @@ export default function agentVentExtension(pi: ExtensionAPI) {
|
|
|
400
437
|
},
|
|
401
438
|
{ source: "agent_vent_tool" },
|
|
402
439
|
);
|
|
440
|
+
throwIfCancelled(signal);
|
|
403
441
|
appendReviewEvent(reviewPath, event);
|
|
404
442
|
const text = [
|
|
405
443
|
`Set local review state for ${event.recurrenceKey} to ${event.state}.`,
|
|
@@ -610,6 +648,7 @@ export default function agentVentExtension(pi: ExtensionAPI) {
|
|
|
610
648
|
});
|
|
611
649
|
}
|
|
612
650
|
if (retentionAction === "archive") {
|
|
651
|
+
throwIfCancelled(signal);
|
|
613
652
|
const result = archiveRecurrenceGroup({
|
|
614
653
|
storePath,
|
|
615
654
|
reviewPath,
|
|
@@ -632,6 +671,7 @@ export default function agentVentExtension(pi: ExtensionAPI) {
|
|
|
632
671
|
retention: result,
|
|
633
672
|
});
|
|
634
673
|
}
|
|
674
|
+
throwIfCancelled(signal);
|
|
635
675
|
const result = restoreRetentionBackup({
|
|
636
676
|
storePath,
|
|
637
677
|
retentionPath,
|
|
@@ -737,15 +777,31 @@ function registerAgentVentCommand(pi: ExtensionAPI, name: string, description: s
|
|
|
737
777
|
description,
|
|
738
778
|
handler: async (args, ctx) => {
|
|
739
779
|
const output = handleCommand(args);
|
|
740
|
-
if (ctx.hasUI) {
|
|
741
|
-
ctx.ui.notify(output, "info");
|
|
742
|
-
} else {
|
|
780
|
+
if (!ctx.hasUI) {
|
|
743
781
|
console.log(output);
|
|
782
|
+
} else if (isLongCommandOutput(output)) {
|
|
783
|
+
pi.sendMessage({
|
|
784
|
+
customType: "agent-vent-command",
|
|
785
|
+
content: output,
|
|
786
|
+
display: true,
|
|
787
|
+
details: { command: name },
|
|
788
|
+
});
|
|
789
|
+
ctx.ui.notify("agent_vent output added to the session transcript", "info");
|
|
790
|
+
} else {
|
|
791
|
+
ctx.ui.notify(output, "info");
|
|
744
792
|
}
|
|
745
793
|
},
|
|
746
794
|
});
|
|
747
795
|
}
|
|
748
796
|
|
|
797
|
+
function isLongCommandOutput(output: string): boolean {
|
|
798
|
+
return output.length > 500 || output.split("\n").length > 8;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function throwIfCancelled(signal: AbortSignal | undefined): void {
|
|
802
|
+
if (signal?.aborted) throw new Error("agent_vent cancelled");
|
|
803
|
+
}
|
|
804
|
+
|
|
749
805
|
function handleCommand(args: string) {
|
|
750
806
|
const tokens = splitCommandArgs(args);
|
|
751
807
|
const action = tokens[0] || "summary";
|
|
@@ -759,6 +815,7 @@ function handleCommand(args: string) {
|
|
|
759
815
|
return [
|
|
760
816
|
"agent_vent commands:",
|
|
761
817
|
" /agent_vent summary Show recurrence groups and candidate incidents.",
|
|
818
|
+
" LLM tool action=preview Preview a local diagnostic record and anti-junk quality check without writing the store.",
|
|
762
819
|
" /agent_vent list [limit] Show recent local vent records.",
|
|
763
820
|
" /agent_vent facets [limit] Show read-only local category/tag/tool/package facets.",
|
|
764
821
|
" /agent_vent review [state|all] [limit] [category=bug] [tag=reload] [tool=pi-reload] [package=tryinget-pi-agent-vent]",
|
|
@@ -1084,8 +1141,8 @@ function parseReviewListTokens(tokens: string[], options: { allowReviewedState?:
|
|
|
1084
1141
|
invalidFilters.push("category=");
|
|
1085
1142
|
continue;
|
|
1086
1143
|
}
|
|
1087
|
-
const
|
|
1088
|
-
if (
|
|
1144
|
+
const category = resolveCategoryFilter(value);
|
|
1145
|
+
if (category) filters.category = category;
|
|
1089
1146
|
else invalidFilters.push(`category=${value}`);
|
|
1090
1147
|
} else if (key === "tool") {
|
|
1091
1148
|
if (!value) {
|
|
@@ -1374,6 +1431,24 @@ function formatDiagnosticWarnings(state: Record<string, unknown>) {
|
|
|
1374
1431
|
: "";
|
|
1375
1432
|
}
|
|
1376
1433
|
|
|
1434
|
+
function formatRecordPreview(
|
|
1435
|
+
record: Record<string, unknown>,
|
|
1436
|
+
quality: { recordable?: boolean; issues?: string[]; warnings?: string[]; boundary?: string },
|
|
1437
|
+
) {
|
|
1438
|
+
const issues = quality.issues || [];
|
|
1439
|
+
const warnings = quality.warnings || [];
|
|
1440
|
+
return [
|
|
1441
|
+
`Agent vent preview: ${quality.recordable ? "recordable" : "not recordable"} (${record.severity || "medium"}/${record.category || "other"}) under ${record.recurrenceKey || "unknown"}.`,
|
|
1442
|
+
`Summary: ${record.summary || "(no summary)"}`,
|
|
1443
|
+
issues.length ? `Issues: ${issues.join("; ")}` : "Issues: none",
|
|
1444
|
+
warnings.length ? `Warnings: ${warnings.join("; ")}` : "Warnings: none",
|
|
1445
|
+
"No local diagnostic record was written. Use action=record only if this is a recurring or review-worthy diagnostic, not an ordinary progress update.",
|
|
1446
|
+
`Boundary: ${quality.boundary || "local diagnostic preview only"}`,
|
|
1447
|
+
].join("\n");
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
export const _test = { isLongCommandOutput, throwIfCancelled };
|
|
1451
|
+
|
|
1377
1452
|
function textResult(text: string, details: Record<string, unknown>) {
|
|
1378
1453
|
return {
|
|
1379
1454
|
content: [{ type: "text" as const, text }],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tryinget/pi-agent-vent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Local pi tool for agents to record recurring frustrations, bugs, and workflow friction without creating incidents or external telemetry",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"access": "public"
|
|
29
29
|
},
|
|
30
30
|
"engines": {
|
|
31
|
-
"node": ">=22"
|
|
31
|
+
"node": ">=22.19.0"
|
|
32
32
|
},
|
|
33
33
|
"scripts": {
|
|
34
34
|
"fix": "bash ./scripts/quality-gate.sh fix",
|
|
@@ -43,7 +43,13 @@
|
|
|
43
43
|
"docs:list:workspace": "bash ./scripts/docs-list.sh --workspace --discover",
|
|
44
44
|
"docs:list:json": "bash ./scripts/docs-list.sh --json",
|
|
45
45
|
"release:check": "bash ./scripts/release-check.sh",
|
|
46
|
-
"release:check:quick": "SKIP_PI_SMOKE=1 bash ./scripts/release-check.sh"
|
|
46
|
+
"release:check:quick": "SKIP_PI_SMOKE=1 bash ./scripts/release-check.sh",
|
|
47
|
+
"loop-doctor": "bash -lc 'node --version; npm --version; npm pkg get name version >/dev/null; git status --short -- . || true; exit 0'",
|
|
48
|
+
"loop-verify-fast": "npm run quality:pre-commit",
|
|
49
|
+
"loop-impact-plan": "bash -lc 'echo \"loop-impact-plan: package-local impact planner is coarse; run npm run loop-impact-run for the full package gate.\"; git status --short -- . || true'",
|
|
50
|
+
"loop-impact-run": "npm run check",
|
|
51
|
+
"loop-impact-wide": "npm run check",
|
|
52
|
+
"loop-landing-check": "npm run check"
|
|
47
53
|
},
|
|
48
54
|
"files": [
|
|
49
55
|
"extensions/agent-vent.ts",
|
|
@@ -60,6 +66,7 @@
|
|
|
60
66
|
"docs/project/product-posture.md",
|
|
61
67
|
"docs/project/2026-05-21-agent-vent-design.md",
|
|
62
68
|
"docs/project/2026-05-21-agent-vent-implementation-plan.md",
|
|
69
|
+
"docs/project/2026-06-05-self-toolbox-agent-vent-diagnostic-boundary.md",
|
|
63
70
|
"docs/project/extension-sop.md",
|
|
64
71
|
"docs/project/trusted-publishing.md",
|
|
65
72
|
"docs/adr/2026-05-22-agent-vent-retention-delete-policy.md"
|
|
@@ -79,17 +86,19 @@
|
|
|
79
86
|
"releaseConfigMode": "component"
|
|
80
87
|
},
|
|
81
88
|
"devDependencies": {
|
|
82
|
-
"@biomejs/biome": "2.3.14"
|
|
89
|
+
"@biomejs/biome": "2.3.14",
|
|
90
|
+
"@earendil-works/pi-ai": "0.80.6",
|
|
91
|
+
"@earendil-works/pi-coding-agent": "0.80.6"
|
|
83
92
|
},
|
|
84
93
|
"overrides": {
|
|
85
94
|
"fast-xml-parser": "5.3.6"
|
|
86
95
|
},
|
|
87
96
|
"peerDependencies": {
|
|
88
|
-
"@
|
|
89
|
-
"@
|
|
97
|
+
"@earendil-works/pi-ai": "*",
|
|
98
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
90
99
|
"typebox": "*"
|
|
91
100
|
},
|
|
92
101
|
"dependencies": {
|
|
93
|
-
"typebox": "
|
|
102
|
+
"typebox": "*"
|
|
94
103
|
}
|
|
95
104
|
}
|
|
@@ -17,6 +17,18 @@
|
|
|
17
17
|
"dependency-governance",
|
|
18
18
|
"specification-and-dsls",
|
|
19
19
|
"engineering-reasoning"
|
|
20
|
-
]
|
|
20
|
+
],
|
|
21
|
+
"loop_validation": {
|
|
22
|
+
"version": "repo-loop-validation-v1",
|
|
23
|
+
"contract_doc": "docs/engineering.local.md#repo-loop-validation",
|
|
24
|
+
"commands": {
|
|
25
|
+
"loop-doctor": "npm run loop-doctor",
|
|
26
|
+
"loop-verify-fast": "npm run loop-verify-fast",
|
|
27
|
+
"loop-impact-plan": "npm run loop-impact-plan",
|
|
28
|
+
"loop-impact-run": "npm run loop-impact-run",
|
|
29
|
+
"loop-impact-wide": "npm run loop-impact-wide",
|
|
30
|
+
"loop-landing-check": "npm run loop-landing-check"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
21
33
|
}
|
|
22
34
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
---
|
|
2
|
-
summary: "
|
|
2
|
+
summary: "pi-agent-vent implementation planning prompt template."
|
|
3
3
|
read_when:
|
|
4
4
|
- "Using or updating the monorepo package implementation-planning prompt template."
|
|
5
|
-
description: Draft an implementation plan for a requested change
|
|
5
|
+
description: Draft an implementation plan for a requested pi-agent-vent change
|
|
6
6
|
system4d:
|
|
7
7
|
container: "Prompt template for implementation planning."
|
|
8
8
|
compass: "Turn requests into actionable, risk-aware plans."
|
|
@@ -10,7 +10,7 @@ system4d:
|
|
|
10
10
|
fog: "Hidden constraints unless assumptions are surfaced."
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
-
Create
|
|
13
|
+
Create a pi-agent-vent implementation plan for this request: $@
|
|
14
14
|
|
|
15
15
|
Include:
|
|
16
16
|
- Scope and non-goals
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
---
|
|
2
|
-
summary: "
|
|
2
|
+
summary: "pi-agent-vent security review prompt template."
|
|
3
3
|
read_when:
|
|
4
4
|
- "Using or updating the monorepo package security-review prompt template."
|
|
5
|
-
description: Review a change for security risks and mitigations
|
|
5
|
+
description: Review a pi-agent-vent change for security risks and mitigations
|
|
6
6
|
system4d:
|
|
7
7
|
container: "Prompt template for security-focused review."
|
|
8
8
|
compass: "Identify practical vulnerabilities before release."
|
|
@@ -10,7 +10,7 @@ system4d:
|
|
|
10
10
|
fog: "Partial context can hide exploit paths."
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
-
Review this change for security concerns: $@
|
|
13
|
+
Review this pi-agent-vent change for security concerns: $@
|
|
14
14
|
|
|
15
15
|
Focus on:
|
|
16
16
|
- Input validation and injection risk
|
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);
|
|
@@ -5,6 +5,7 @@ import path from "node:path";
|
|
|
5
5
|
import test from "node:test";
|
|
6
6
|
import {
|
|
7
7
|
assertAgentVentPathSmokeOutput,
|
|
8
|
+
assertExactHostContract,
|
|
8
9
|
assertInstalledArtifactPackage,
|
|
9
10
|
assertLocalTarballInstallSource,
|
|
10
11
|
assertPackageSpecInstalled,
|
|
@@ -13,6 +14,24 @@ import {
|
|
|
13
14
|
packageSourcesFromSettings,
|
|
14
15
|
} from "../scripts/release-smoke-check.mjs";
|
|
15
16
|
|
|
17
|
+
test("release smoke release-age bypass is gated by one exact host contract", () => {
|
|
18
|
+
const packageJson = {
|
|
19
|
+
devDependencies: {
|
|
20
|
+
"@earendil-works/pi-ai": "0.80.6",
|
|
21
|
+
"@earendil-works/pi-coding-agent": "0.80.6",
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
assert.equal(assertExactHostContract({ packageJson, hostVersion: "0.80.6" }), "0.80.6");
|
|
25
|
+
assert.throws(
|
|
26
|
+
() => assertExactHostContract({ packageJson, hostVersion: "^0.80.6" }),
|
|
27
|
+
/requires an exact Pi host version/,
|
|
28
|
+
);
|
|
29
|
+
assert.throws(
|
|
30
|
+
() => assertExactHostContract({ packageJson, hostVersion: "0.80.7" }),
|
|
31
|
+
/host contract mismatch/,
|
|
32
|
+
);
|
|
33
|
+
});
|
|
34
|
+
|
|
16
35
|
test("release smoke settings check accepts only unfiltered string package entries", () => {
|
|
17
36
|
const settings = {
|
|
18
37
|
packages: [
|
package/tests/vent-store.test.js
CHANGED
|
@@ -111,6 +111,30 @@ test("createVentRecord minimizes, redacts, and derives recurrence key", () => {
|
|
|
111
111
|
assert.deepEqual(tagOnlySecret.privacy.redactionPatterns, ["assigned_secret"]);
|
|
112
112
|
});
|
|
113
113
|
|
|
114
|
+
test("category aliases normalize at record and review-filter boundaries", () => {
|
|
115
|
+
const workflow = createVentRecord({
|
|
116
|
+
summary: "Workflow alias should not degrade",
|
|
117
|
+
category: "workflow_friction",
|
|
118
|
+
recurrenceKey: "workflow alias",
|
|
119
|
+
});
|
|
120
|
+
const missing = createVentRecord({
|
|
121
|
+
summary: "Missing affordance alias should preserve meaning",
|
|
122
|
+
category: "missing_affordance",
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
assert.equal(workflow.category, "workflow");
|
|
126
|
+
assert.equal(workflow.recurrenceKey, "workflow:workflow-alias");
|
|
127
|
+
assert.equal(missing.category, "missing_capability");
|
|
128
|
+
assert.match(missing.recurrenceKey, /^missing_capability:/);
|
|
129
|
+
|
|
130
|
+
const queue = summarizeReviewQueue([workflow, missing], [], {
|
|
131
|
+
filters: { category: "workflow_friction" },
|
|
132
|
+
});
|
|
133
|
+
assert.equal(queue.filters.category, "workflow");
|
|
134
|
+
assert.equal(queue.matchingGroupCount, 1);
|
|
135
|
+
assert.equal(queue.items[0].recurrenceKey, workflow.recurrenceKey);
|
|
136
|
+
});
|
|
137
|
+
|
|
114
138
|
test("explicit recurrence keys are redacted before slugging", () => {
|
|
115
139
|
const record = createVentRecord({
|
|
116
140
|
summary: "Explicit key should not leak",
|