@tryinget/pi-agent-vent 0.1.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/LICENSE +78 -0
- package/README.md +188 -0
- package/biome.jsonc +43 -0
- package/docs/adr/2026-05-22-agent-vent-retention-delete-policy.md +65 -0
- package/docs/engineering.local.md +57 -0
- package/docs/project/2026-05-21-agent-vent-design.md +182 -0
- package/docs/project/2026-05-21-agent-vent-implementation-plan.md +66 -0
- package/docs/project/extension-sop.md +52 -0
- package/docs/project/product-posture.md +191 -0
- package/docs/project/trusted-publishing.md +53 -0
- package/docs/project/vision.md +70 -0
- package/examples/.gitkeep +0 -0
- package/extensions/agent-vent.ts +1384 -0
- package/package.json +95 -0
- package/policy/engineering-lane.json +22 -0
- package/policy/security-policy.json +10 -0
- package/prompts/implementation-planning.md +20 -0
- package/prompts/security-review.md +20 -0
- package/scripts/docs-list.sh +50 -0
- package/scripts/quality-gate.sh +168 -0
- package/scripts/release-artifact-check.mjs +221 -0
- package/scripts/release-check.sh +196 -0
- package/scripts/release-smoke-check.mjs +367 -0
- package/scripts/release-smoke.sh +90 -0
- package/scripts/validate-structure.mjs +248 -0
- package/scripts/validate-structure.sh +145 -0
- package/src/vent-store.js +2839 -0
- package/tests/.gitkeep +0 -0
- package/tests/agent-vent-extension.test.js +762 -0
- package/tests/release-artifact-check.test.js +168 -0
- package/tests/release-smoke-check.test.js +235 -0
- package/tests/vent-store.test.js +1972 -0
|
@@ -0,0 +1,1384 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
3
|
+
import { Type } from "typebox";
|
|
4
|
+
import {
|
|
5
|
+
appendCurationEvent,
|
|
6
|
+
appendReviewEvent,
|
|
7
|
+
appendVentRecord,
|
|
8
|
+
archiveRecurrenceGroup,
|
|
9
|
+
assertCanCurateRecurrence,
|
|
10
|
+
buildEscalationDraft,
|
|
11
|
+
buildFacetSummary,
|
|
12
|
+
buildLifecycleSnapshot,
|
|
13
|
+
buildRetentionCandidates,
|
|
14
|
+
buildRetentionHistory,
|
|
15
|
+
buildRetentionPreview,
|
|
16
|
+
buildReviewComparison,
|
|
17
|
+
buildReviewDetail,
|
|
18
|
+
buildReviewOutcomes,
|
|
19
|
+
CATEGORIES,
|
|
20
|
+
CURATION_ACTIONS,
|
|
21
|
+
clampLimit,
|
|
22
|
+
createCurationEvent,
|
|
23
|
+
createReviewEvent,
|
|
24
|
+
createVentRecord,
|
|
25
|
+
DRAFT_TARGETS,
|
|
26
|
+
defaultBackupDir,
|
|
27
|
+
defaultCurationPath,
|
|
28
|
+
defaultRetentionPath,
|
|
29
|
+
defaultReviewPath,
|
|
30
|
+
defaultStorePath,
|
|
31
|
+
formatExportJson,
|
|
32
|
+
formatExportMarkdown,
|
|
33
|
+
formatFacetSummary,
|
|
34
|
+
formatLifecycleStats,
|
|
35
|
+
formatPath,
|
|
36
|
+
formatRecent,
|
|
37
|
+
formatRetentionArchiveResult,
|
|
38
|
+
formatRetentionCandidates,
|
|
39
|
+
formatRetentionHistory,
|
|
40
|
+
formatRetentionPreview,
|
|
41
|
+
formatRetentionRestoreResult,
|
|
42
|
+
formatReviewComparison,
|
|
43
|
+
formatReviewDetail,
|
|
44
|
+
formatReviewOutcomes,
|
|
45
|
+
formatReviewQueue,
|
|
46
|
+
formatSummary,
|
|
47
|
+
loadDiagnosticState,
|
|
48
|
+
normalizeRetentionAction,
|
|
49
|
+
normalizeReviewState,
|
|
50
|
+
RETENTION_ACTIONS,
|
|
51
|
+
REVIEW_STATES,
|
|
52
|
+
readRetentionEvents,
|
|
53
|
+
resolveRecurrenceGroup,
|
|
54
|
+
restoreRetentionBackup,
|
|
55
|
+
SEVERITIES,
|
|
56
|
+
summarizeRecords,
|
|
57
|
+
summarizeReviewQueue,
|
|
58
|
+
} from "../src/vent-store.js";
|
|
59
|
+
|
|
60
|
+
const ACTIONS = [
|
|
61
|
+
"record",
|
|
62
|
+
"summary",
|
|
63
|
+
"list",
|
|
64
|
+
"path",
|
|
65
|
+
"review",
|
|
66
|
+
"outcomes",
|
|
67
|
+
"compare",
|
|
68
|
+
"set_review",
|
|
69
|
+
"stats",
|
|
70
|
+
"export",
|
|
71
|
+
"facets",
|
|
72
|
+
"curate",
|
|
73
|
+
"draft",
|
|
74
|
+
"retention",
|
|
75
|
+
] as const;
|
|
76
|
+
const EXPORT_FORMATS = ["markdown", "json"] as const;
|
|
77
|
+
const RETENTION_CANDIDATE_STATES = ["reviewed", "all", ...REVIEW_STATES] as const;
|
|
78
|
+
|
|
79
|
+
const AgentVentParams = Type.Object({
|
|
80
|
+
action: Type.Optional(
|
|
81
|
+
Type.Union(
|
|
82
|
+
ACTIONS.map((action) => Type.Literal(action)),
|
|
83
|
+
{
|
|
84
|
+
description:
|
|
85
|
+
"Action to perform. Defaults to record when summary is provided; otherwise summary.",
|
|
86
|
+
},
|
|
87
|
+
),
|
|
88
|
+
),
|
|
89
|
+
category: Type.Optional(
|
|
90
|
+
Type.Union(
|
|
91
|
+
CATEGORIES.map((category) => Type.Literal(category)),
|
|
92
|
+
{
|
|
93
|
+
description: "Local category for the frustration pattern.",
|
|
94
|
+
},
|
|
95
|
+
),
|
|
96
|
+
),
|
|
97
|
+
severity: Type.Optional(
|
|
98
|
+
Type.Union(
|
|
99
|
+
SEVERITIES.map((severity) => Type.Literal(severity)),
|
|
100
|
+
{
|
|
101
|
+
description: "Local severity estimate. Candidate incidents are still advisory only.",
|
|
102
|
+
},
|
|
103
|
+
),
|
|
104
|
+
),
|
|
105
|
+
summary: Type.Optional(
|
|
106
|
+
Type.String({
|
|
107
|
+
description:
|
|
108
|
+
"Short minimized summary. Required for action=record. Do not include secrets or raw user payloads.",
|
|
109
|
+
}),
|
|
110
|
+
),
|
|
111
|
+
frustration: Type.Optional(
|
|
112
|
+
Type.String({ description: "Brief explanation of why this is frustrating or costly." }),
|
|
113
|
+
),
|
|
114
|
+
evidence: Type.Optional(
|
|
115
|
+
Type.String({
|
|
116
|
+
description: "Minimal evidence or observation. Prefer pointers over copied logs.",
|
|
117
|
+
}),
|
|
118
|
+
),
|
|
119
|
+
expected: Type.Optional(Type.String({ description: "What the agent expected to happen." })),
|
|
120
|
+
actual: Type.Optional(Type.String({ description: "What actually happened." })),
|
|
121
|
+
reproduction: Type.Optional(
|
|
122
|
+
Type.String({ description: "Minimal reproduction hint if known; avoid sensitive inputs." }),
|
|
123
|
+
),
|
|
124
|
+
tool: Type.Optional(
|
|
125
|
+
Type.String({ description: "Optional local diagnostic tool facet. Not owner routing." }),
|
|
126
|
+
),
|
|
127
|
+
packageName: Type.Optional(
|
|
128
|
+
Type.String({ description: "Optional local diagnostic package facet. Not owner routing." }),
|
|
129
|
+
),
|
|
130
|
+
recurrenceKey: Type.Optional(
|
|
131
|
+
Type.String({
|
|
132
|
+
description:
|
|
133
|
+
"Stable grouping key for repeated occurrences, or the target recurrence key for action=set_review.",
|
|
134
|
+
}),
|
|
135
|
+
),
|
|
136
|
+
curationAction: Type.Optional(
|
|
137
|
+
Type.Union(
|
|
138
|
+
CURATION_ACTIONS.map((action) => Type.Literal(action)),
|
|
139
|
+
{
|
|
140
|
+
description: "Local curation action for action=curate.",
|
|
141
|
+
},
|
|
142
|
+
),
|
|
143
|
+
),
|
|
144
|
+
sourceRecurrenceKey: Type.Optional(
|
|
145
|
+
Type.String({ description: "Existing recurrence group key to curate for action=curate." }),
|
|
146
|
+
),
|
|
147
|
+
targetRecurrenceKey: Type.Optional(
|
|
148
|
+
Type.String({ description: "Target projected recurrence group key for action=curate." }),
|
|
149
|
+
),
|
|
150
|
+
curationNote: Type.Optional(
|
|
151
|
+
Type.String({
|
|
152
|
+
description:
|
|
153
|
+
"Optional minimized local curation note for action=curate. Do not include secrets or raw payloads.",
|
|
154
|
+
}),
|
|
155
|
+
),
|
|
156
|
+
reviewState: Type.Optional(
|
|
157
|
+
Type.Union(
|
|
158
|
+
REVIEW_STATES.map((state) => Type.Literal(state)),
|
|
159
|
+
{
|
|
160
|
+
description:
|
|
161
|
+
"Local review state filter for action=review/outcomes or target state for action=set_review.",
|
|
162
|
+
},
|
|
163
|
+
),
|
|
164
|
+
),
|
|
165
|
+
retentionCandidateState: Type.Optional(
|
|
166
|
+
Type.Union(
|
|
167
|
+
RETENTION_CANDIDATE_STATES.map((state) => Type.Literal(state)),
|
|
168
|
+
{
|
|
169
|
+
description:
|
|
170
|
+
"Local state filter for action=retention with retentionAction=candidates. Defaults to reviewed; reviewed means acknowledged, dismissed, or escalation_drafted.",
|
|
171
|
+
},
|
|
172
|
+
),
|
|
173
|
+
),
|
|
174
|
+
reviewNote: Type.Optional(
|
|
175
|
+
Type.String({
|
|
176
|
+
description:
|
|
177
|
+
"Optional minimized local review note for action=set_review. Do not include secrets or raw payloads.",
|
|
178
|
+
}),
|
|
179
|
+
),
|
|
180
|
+
exportFormat: Type.Optional(
|
|
181
|
+
Type.Union(
|
|
182
|
+
EXPORT_FORMATS.map((format) => Type.Literal(format)),
|
|
183
|
+
{
|
|
184
|
+
description: "Projection export format for action=export. Defaults to markdown.",
|
|
185
|
+
},
|
|
186
|
+
),
|
|
187
|
+
),
|
|
188
|
+
draftTarget: Type.Optional(
|
|
189
|
+
Type.Union(
|
|
190
|
+
DRAFT_TARGETS.map((target) => Type.Literal(target)),
|
|
191
|
+
{
|
|
192
|
+
description: "Draft-only owner surface text target for action=draft.",
|
|
193
|
+
},
|
|
194
|
+
),
|
|
195
|
+
),
|
|
196
|
+
retentionAction: Type.Optional(
|
|
197
|
+
Type.Union(
|
|
198
|
+
RETENTION_ACTIONS.map((action) => Type.Literal(action)),
|
|
199
|
+
{
|
|
200
|
+
description:
|
|
201
|
+
"Local data lifecycle action for action=retention: candidates, history, preview, archive, or restore. Only archive/restore mutate local diagnostic stores and require confirmation tokens.",
|
|
202
|
+
},
|
|
203
|
+
),
|
|
204
|
+
),
|
|
205
|
+
confirmationToken: Type.Optional(
|
|
206
|
+
Type.String({ description: "Exact retention confirmation token shown by preview or archive." }),
|
|
207
|
+
),
|
|
208
|
+
backupPath: Type.Optional(
|
|
209
|
+
Type.String({
|
|
210
|
+
description: "Package-created retention backup path for action=retention restore.",
|
|
211
|
+
}),
|
|
212
|
+
),
|
|
213
|
+
retentionNote: Type.Optional(
|
|
214
|
+
Type.String({
|
|
215
|
+
description: "Optional minimized local retention note. Do not include secrets.",
|
|
216
|
+
}),
|
|
217
|
+
),
|
|
218
|
+
tags: Type.Optional(Type.Array(Type.String(), { description: "Optional local grouping tags." })),
|
|
219
|
+
limit: Type.Optional(
|
|
220
|
+
Type.Integer({
|
|
221
|
+
minimum: 1,
|
|
222
|
+
maximum: 100,
|
|
223
|
+
description:
|
|
224
|
+
"Maximum rows/groups for list, summary, review, outcomes, compare, export, or retention planning actions.",
|
|
225
|
+
}),
|
|
226
|
+
),
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
export default function agentVentExtension(pi: ExtensionAPI) {
|
|
230
|
+
pi.registerTool({
|
|
231
|
+
name: "agent_vent",
|
|
232
|
+
label: "Agent Vent",
|
|
233
|
+
description:
|
|
234
|
+
"Record, review, and inspect local agent frustration events so recurring bugs, workflow friction, and missing affordances become visible.",
|
|
235
|
+
promptSnippet:
|
|
236
|
+
"Record minimized local frustration events and review recurring patterns without creating incidents, tasks, issues, evidence records, or telemetry.",
|
|
237
|
+
promptGuidelines: [
|
|
238
|
+
"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.",
|
|
239
|
+
"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.",
|
|
240
|
+
"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.",
|
|
241
|
+
"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.",
|
|
242
|
+
"Use action=facets for read-only local category/tag/tool/package triage; facets and review filters are caller-supplied diagnostic labels, not owner routing.",
|
|
243
|
+
"Use action=stats or action=export for non-destructive local lifecycle inspection; exports are diagnostic projections, not evidence or escalation. Export may be focused by local category/tag/tool/package facets; those facets are not owner routing.",
|
|
244
|
+
"Use action=curate to append local recurrence merge/rename projection events; raw vent records are not rewritten.",
|
|
245
|
+
"Use action=draft to generate owner-surface draft text only; never claim it submitted, filed, declared, or recorded anything.",
|
|
246
|
+
"Use action=retention to list read-only archive candidates or retention history, preview, confirmation-gate, archive, or restore local diagnostic records only; never imply owner-system deletion or canonical evidence changes.",
|
|
247
|
+
"Do not use agent_vent for ordinary progress updates, single-use complaints, or content that belongs in the final answer.",
|
|
248
|
+
"agent_vent records and review states are local diagnostics only; agent_vent must not claim to create AK tasks, incidents, GitHub issues, canonical evidence, external telemetry, or ASC/self state.",
|
|
249
|
+
"When calling agent_vent, summarize minimally and never include secrets, credentials, private user payloads, or long raw logs.",
|
|
250
|
+
],
|
|
251
|
+
parameters: AgentVentParams,
|
|
252
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
253
|
+
const storePath = defaultStorePath();
|
|
254
|
+
const reviewPath = defaultReviewPath();
|
|
255
|
+
const curationPath = defaultCurationPath();
|
|
256
|
+
const retentionPath = defaultRetentionPath();
|
|
257
|
+
const backupDir = defaultBackupDir();
|
|
258
|
+
const action = params.action || (params.summary ? "record" : "summary");
|
|
259
|
+
|
|
260
|
+
if (action === "path") {
|
|
261
|
+
return textResult(
|
|
262
|
+
formatPath(storePath, reviewPath, curationPath, retentionPath, backupDir),
|
|
263
|
+
{
|
|
264
|
+
action,
|
|
265
|
+
storePath,
|
|
266
|
+
reviewPath,
|
|
267
|
+
curationPath,
|
|
268
|
+
retentionPath,
|
|
269
|
+
backupDir,
|
|
270
|
+
},
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (action === "record") {
|
|
275
|
+
const sessionFile = ctx.sessionManager.getSessionFile();
|
|
276
|
+
const record = createVentRecord(params, {
|
|
277
|
+
cwd: ctx.cwd,
|
|
278
|
+
sessionFile: sessionFile ? path.basename(sessionFile) : undefined,
|
|
279
|
+
source: "agent_vent_tool",
|
|
280
|
+
});
|
|
281
|
+
appendVentRecord(storePath, record);
|
|
282
|
+
const state = loadDiagnosticState({
|
|
283
|
+
storePath,
|
|
284
|
+
reviewPath,
|
|
285
|
+
curationPath,
|
|
286
|
+
retentionPath,
|
|
287
|
+
backupDir,
|
|
288
|
+
});
|
|
289
|
+
const { records, curationEvents, malformedLines } = state;
|
|
290
|
+
const group =
|
|
291
|
+
resolveRecurrenceGroup(records, record.recurrenceKey, curationEvents)?.group ||
|
|
292
|
+
summarizeRecords(
|
|
293
|
+
records.filter((entry) => entry.recurrenceKey === record.recurrenceKey),
|
|
294
|
+
{ limit: 1, curationEvents },
|
|
295
|
+
).groups[0];
|
|
296
|
+
const candidate = group?.candidateIncident ? " Candidate incident for human review." : "";
|
|
297
|
+
const text = `Recorded agent vent ${record.id} (${record.severity}/${record.category}) under ${record.recurrenceKey}.${candidate}`;
|
|
298
|
+
return textResult(text, {
|
|
299
|
+
action,
|
|
300
|
+
storePath,
|
|
301
|
+
record,
|
|
302
|
+
recurrenceGroup: group,
|
|
303
|
+
malformedLines,
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (
|
|
308
|
+
action === "retention" &&
|
|
309
|
+
normalizeRetentionAction(params.retentionAction || "preview") === "history"
|
|
310
|
+
) {
|
|
311
|
+
const retentions = readRetentionEvents(retentionPath);
|
|
312
|
+
const history = buildRetentionHistory({
|
|
313
|
+
retentionEvents: retentions.events,
|
|
314
|
+
backupDir,
|
|
315
|
+
limit: clampLimit(params.limit, 20),
|
|
316
|
+
});
|
|
317
|
+
const suffix = formatRetentionReadWarnings(retentions);
|
|
318
|
+
return textResult(`${formatRetentionHistory(history)}${suffix}`, {
|
|
319
|
+
action,
|
|
320
|
+
retentionAction: "history",
|
|
321
|
+
retentionPath,
|
|
322
|
+
backupDir,
|
|
323
|
+
retention: history,
|
|
324
|
+
malformedRetentionLines: retentions.malformedLines,
|
|
325
|
+
oversizedRetentionLines: retentions.oversizedLines,
|
|
326
|
+
invalidRetentionEvents: retentions.invalidEvents,
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const state = loadDiagnosticState({
|
|
331
|
+
storePath,
|
|
332
|
+
reviewPath,
|
|
333
|
+
curationPath,
|
|
334
|
+
retentionPath,
|
|
335
|
+
backupDir,
|
|
336
|
+
});
|
|
337
|
+
const {
|
|
338
|
+
records,
|
|
339
|
+
reviewEvents,
|
|
340
|
+
curationEvents,
|
|
341
|
+
retentionEvents,
|
|
342
|
+
malformedLines,
|
|
343
|
+
malformedReviewLines,
|
|
344
|
+
malformedCurationLines,
|
|
345
|
+
malformedRetentionLines,
|
|
346
|
+
oversizedLines,
|
|
347
|
+
oversizedReviewLines,
|
|
348
|
+
oversizedCurationLines,
|
|
349
|
+
oversizedRetentionLines,
|
|
350
|
+
invalidRecords,
|
|
351
|
+
invalidReviewEvents,
|
|
352
|
+
invalidCurationEvents,
|
|
353
|
+
invalidRetentionEvents,
|
|
354
|
+
quarantinedCurationEvents,
|
|
355
|
+
} = state;
|
|
356
|
+
|
|
357
|
+
if (action === "curate") {
|
|
358
|
+
const input = {
|
|
359
|
+
action: params.curationAction,
|
|
360
|
+
sourceRecurrenceKey: params.sourceRecurrenceKey,
|
|
361
|
+
targetRecurrenceKey: params.targetRecurrenceKey,
|
|
362
|
+
note: params.curationNote,
|
|
363
|
+
};
|
|
364
|
+
assertCanCurateRecurrence(records, curationEvents, input);
|
|
365
|
+
const event = createCurationEvent(input, { source: "agent_vent_tool" });
|
|
366
|
+
appendCurationEvent(curationPath, event);
|
|
367
|
+
const targetText = event.targetRecurrenceKey ? ` -> ${event.targetRecurrenceKey}` : "";
|
|
368
|
+
const text = [
|
|
369
|
+
`Recorded local recurrence ${event.action} curation: ${event.sourceRecurrenceKey}${targetText}.`,
|
|
370
|
+
"Boundary: local diagnostic curation projection only; raw vents were not rewritten and no AK task, GitHub issue, incident, evidence, telemetry, or ASC/self state was created.",
|
|
371
|
+
].join("\n");
|
|
372
|
+
return textResult(text, {
|
|
373
|
+
action,
|
|
374
|
+
storePath,
|
|
375
|
+
reviewPath,
|
|
376
|
+
curationPath,
|
|
377
|
+
curationEvent: event,
|
|
378
|
+
malformedLines,
|
|
379
|
+
malformedCurationLines,
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (action === "set_review") {
|
|
384
|
+
const recurrenceKey = params.recurrenceKey?.trim();
|
|
385
|
+
if (!recurrenceKey) throw new Error("action=set_review requires recurrenceKey");
|
|
386
|
+
if (!params.reviewState) throw new Error("action=set_review requires reviewState");
|
|
387
|
+
const resolved = resolveRecurrenceGroup(records, recurrenceKey, curationEvents);
|
|
388
|
+
if (!resolved) {
|
|
389
|
+
throw new Error(`cannot set review state for unknown recurrence group: ${recurrenceKey}`);
|
|
390
|
+
}
|
|
391
|
+
const event = createReviewEvent(
|
|
392
|
+
{
|
|
393
|
+
recurrenceKey: resolved.recurrenceKey,
|
|
394
|
+
state: params.reviewState,
|
|
395
|
+
note: params.reviewNote,
|
|
396
|
+
},
|
|
397
|
+
{ source: "agent_vent_tool" },
|
|
398
|
+
);
|
|
399
|
+
appendReviewEvent(reviewPath, event);
|
|
400
|
+
const text = [
|
|
401
|
+
`Set local review state for ${event.recurrenceKey} to ${event.state}.`,
|
|
402
|
+
"Boundary: local diagnostic review state only; no AK task, GitHub issue, incident, evidence, telemetry, or ASC/self state was created.",
|
|
403
|
+
].join("\n");
|
|
404
|
+
return textResult(text, {
|
|
405
|
+
action,
|
|
406
|
+
storePath,
|
|
407
|
+
reviewPath,
|
|
408
|
+
curationPath,
|
|
409
|
+
reviewEvent: event,
|
|
410
|
+
malformedLines,
|
|
411
|
+
malformedCurationLines,
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (action === "list") {
|
|
416
|
+
return textResult(formatRecent(records, clampLimit(params.limit)), {
|
|
417
|
+
action,
|
|
418
|
+
storePath,
|
|
419
|
+
count: records.length,
|
|
420
|
+
malformedLines,
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (action === "facets") {
|
|
425
|
+
const facets = buildFacetSummary({
|
|
426
|
+
records,
|
|
427
|
+
reviewEvents,
|
|
428
|
+
curationEvents,
|
|
429
|
+
limit: clampLimit(params.limit, 10),
|
|
430
|
+
});
|
|
431
|
+
return textResult(formatFacetSummary(facets), {
|
|
432
|
+
action,
|
|
433
|
+
storePath,
|
|
434
|
+
reviewPath,
|
|
435
|
+
curationPath,
|
|
436
|
+
facets,
|
|
437
|
+
malformedLines,
|
|
438
|
+
malformedReviewLines,
|
|
439
|
+
malformedCurationLines,
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if (action === "outcomes") {
|
|
444
|
+
const outcomes = buildReviewOutcomes({
|
|
445
|
+
records,
|
|
446
|
+
reviewEvents,
|
|
447
|
+
curationEvents,
|
|
448
|
+
state: params.reviewState || "all",
|
|
449
|
+
limit: clampLimit(params.limit, 5),
|
|
450
|
+
filters: {
|
|
451
|
+
category: params.category,
|
|
452
|
+
tool: params.tool,
|
|
453
|
+
packageName: params.packageName,
|
|
454
|
+
tags: params.tags,
|
|
455
|
+
},
|
|
456
|
+
});
|
|
457
|
+
return textResult(formatReviewOutcomes(outcomes), {
|
|
458
|
+
action,
|
|
459
|
+
storePath,
|
|
460
|
+
reviewPath,
|
|
461
|
+
curationPath,
|
|
462
|
+
reviewOutcomes: outcomes,
|
|
463
|
+
malformedLines,
|
|
464
|
+
malformedReviewLines,
|
|
465
|
+
malformedCurationLines,
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (action === "compare") {
|
|
470
|
+
const comparison = buildReviewComparison({
|
|
471
|
+
records,
|
|
472
|
+
reviewEvents,
|
|
473
|
+
curationEvents,
|
|
474
|
+
limit: clampLimit(params.limit, 5),
|
|
475
|
+
filters: {
|
|
476
|
+
category: params.category,
|
|
477
|
+
tool: params.tool,
|
|
478
|
+
packageName: params.packageName,
|
|
479
|
+
tags: params.tags,
|
|
480
|
+
},
|
|
481
|
+
});
|
|
482
|
+
return textResult(formatReviewComparison(comparison), {
|
|
483
|
+
action,
|
|
484
|
+
storePath,
|
|
485
|
+
reviewPath,
|
|
486
|
+
curationPath,
|
|
487
|
+
reviewComparison: comparison,
|
|
488
|
+
malformedLines,
|
|
489
|
+
malformedReviewLines,
|
|
490
|
+
malformedCurationLines,
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
if (action === "review") {
|
|
495
|
+
if (params.recurrenceKey) {
|
|
496
|
+
const detail = buildReviewDetail({
|
|
497
|
+
recurrenceKey: params.recurrenceKey,
|
|
498
|
+
records,
|
|
499
|
+
reviewEvents,
|
|
500
|
+
curationEvents,
|
|
501
|
+
limit: clampLimit(params.limit, 5),
|
|
502
|
+
});
|
|
503
|
+
return textResult(formatReviewDetail(detail), {
|
|
504
|
+
action,
|
|
505
|
+
storePath,
|
|
506
|
+
reviewPath,
|
|
507
|
+
curationPath,
|
|
508
|
+
reviewDetail: detail,
|
|
509
|
+
malformedLines,
|
|
510
|
+
malformedReviewLines,
|
|
511
|
+
malformedCurationLines,
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
const queue = summarizeReviewQueue(records, reviewEvents, {
|
|
515
|
+
state: params.reviewState,
|
|
516
|
+
limit: clampLimit(params.limit, 20),
|
|
517
|
+
curationEvents,
|
|
518
|
+
filters: {
|
|
519
|
+
category: params.category,
|
|
520
|
+
tool: params.tool,
|
|
521
|
+
packageName: params.packageName,
|
|
522
|
+
tags: params.tags,
|
|
523
|
+
},
|
|
524
|
+
});
|
|
525
|
+
return textResult(formatReviewQueue(queue), {
|
|
526
|
+
action,
|
|
527
|
+
storePath,
|
|
528
|
+
reviewPath,
|
|
529
|
+
curationPath,
|
|
530
|
+
reviewQueue: queue,
|
|
531
|
+
malformedLines,
|
|
532
|
+
malformedReviewLines,
|
|
533
|
+
malformedCurationLines,
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
if (action === "draft") {
|
|
538
|
+
const draft = buildEscalationDraft({
|
|
539
|
+
target: params.draftTarget,
|
|
540
|
+
recurrenceKey: params.recurrenceKey,
|
|
541
|
+
records,
|
|
542
|
+
reviewEvents,
|
|
543
|
+
curationEvents,
|
|
544
|
+
limit: clampLimit(params.limit, 5),
|
|
545
|
+
});
|
|
546
|
+
return textResult(draft.text, {
|
|
547
|
+
action,
|
|
548
|
+
draftTarget: draft.target,
|
|
549
|
+
storePath,
|
|
550
|
+
reviewPath,
|
|
551
|
+
curationPath,
|
|
552
|
+
draft,
|
|
553
|
+
malformedLines,
|
|
554
|
+
malformedReviewLines,
|
|
555
|
+
malformedCurationLines,
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
if (action === "retention") {
|
|
560
|
+
const retentionAction = normalizeRetentionAction(params.retentionAction || "preview");
|
|
561
|
+
if (retentionAction === "candidates") {
|
|
562
|
+
const candidates = buildRetentionCandidates({
|
|
563
|
+
records,
|
|
564
|
+
reviewEvents,
|
|
565
|
+
curationEvents,
|
|
566
|
+
state: params.retentionCandidateState || params.reviewState,
|
|
567
|
+
limit: clampLimit(params.limit, 20),
|
|
568
|
+
filters: {
|
|
569
|
+
category: params.category,
|
|
570
|
+
tool: params.tool,
|
|
571
|
+
packageName: params.packageName,
|
|
572
|
+
tags: params.tags,
|
|
573
|
+
},
|
|
574
|
+
});
|
|
575
|
+
return textResult(formatRetentionCandidates(candidates), {
|
|
576
|
+
action,
|
|
577
|
+
retentionAction,
|
|
578
|
+
storePath,
|
|
579
|
+
reviewPath,
|
|
580
|
+
curationPath,
|
|
581
|
+
retentionPath,
|
|
582
|
+
backupDir,
|
|
583
|
+
retention: candidates,
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
if (retentionAction === "preview") {
|
|
587
|
+
const preview = buildRetentionPreview({
|
|
588
|
+
recurrenceKey: params.recurrenceKey,
|
|
589
|
+
records,
|
|
590
|
+
reviewEvents,
|
|
591
|
+
curationEvents,
|
|
592
|
+
storeHash: state.ventsHash,
|
|
593
|
+
reviewHash: state.reviewEventsHash,
|
|
594
|
+
curationHash: state.curationEventsHash,
|
|
595
|
+
limit: clampLimit(params.limit, 5),
|
|
596
|
+
});
|
|
597
|
+
return textResult(formatRetentionPreview(preview), {
|
|
598
|
+
action,
|
|
599
|
+
retentionAction,
|
|
600
|
+
storePath,
|
|
601
|
+
reviewPath,
|
|
602
|
+
curationPath,
|
|
603
|
+
retentionPath,
|
|
604
|
+
backupDir,
|
|
605
|
+
retention: preview,
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
if (retentionAction === "archive") {
|
|
609
|
+
const result = archiveRecurrenceGroup({
|
|
610
|
+
storePath,
|
|
611
|
+
reviewPath,
|
|
612
|
+
curationPath,
|
|
613
|
+
retentionPath,
|
|
614
|
+
backupDir,
|
|
615
|
+
recurrenceKey: params.recurrenceKey,
|
|
616
|
+
confirmationToken: params.confirmationToken,
|
|
617
|
+
note: params.retentionNote,
|
|
618
|
+
source: "agent_vent_tool",
|
|
619
|
+
});
|
|
620
|
+
return textResult(formatRetentionArchiveResult(result), {
|
|
621
|
+
action,
|
|
622
|
+
retentionAction,
|
|
623
|
+
storePath,
|
|
624
|
+
reviewPath,
|
|
625
|
+
curationPath,
|
|
626
|
+
retentionPath,
|
|
627
|
+
backupDir,
|
|
628
|
+
retention: result,
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
const result = restoreRetentionBackup({
|
|
632
|
+
storePath,
|
|
633
|
+
retentionPath,
|
|
634
|
+
backupDir,
|
|
635
|
+
backupPath: params.backupPath,
|
|
636
|
+
confirmationToken: params.confirmationToken,
|
|
637
|
+
note: params.retentionNote,
|
|
638
|
+
source: "agent_vent_tool",
|
|
639
|
+
});
|
|
640
|
+
return textResult(formatRetentionRestoreResult(result), {
|
|
641
|
+
action,
|
|
642
|
+
retentionAction,
|
|
643
|
+
storePath,
|
|
644
|
+
retentionPath,
|
|
645
|
+
backupDir,
|
|
646
|
+
retention: result,
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
if (action === "stats" || action === "export") {
|
|
651
|
+
const snapshot = buildLifecycleSnapshot({
|
|
652
|
+
records,
|
|
653
|
+
reviewEvents,
|
|
654
|
+
curationEvents,
|
|
655
|
+
retentionEvents,
|
|
656
|
+
storePath,
|
|
657
|
+
reviewPath,
|
|
658
|
+
curationPath,
|
|
659
|
+
retentionPath,
|
|
660
|
+
backupDir,
|
|
661
|
+
malformedLines,
|
|
662
|
+
malformedReviewLines,
|
|
663
|
+
malformedCurationLines,
|
|
664
|
+
malformedRetentionLines,
|
|
665
|
+
oversizedLines,
|
|
666
|
+
oversizedReviewLines,
|
|
667
|
+
oversizedCurationLines,
|
|
668
|
+
oversizedRetentionLines,
|
|
669
|
+
invalidRecords,
|
|
670
|
+
invalidReviewEvents,
|
|
671
|
+
invalidCurationEvents,
|
|
672
|
+
invalidRetentionEvents,
|
|
673
|
+
quarantinedCurationEvents,
|
|
674
|
+
state: params.reviewState || "all",
|
|
675
|
+
limit: clampLimit(params.limit, 20),
|
|
676
|
+
filters:
|
|
677
|
+
action === "export"
|
|
678
|
+
? {
|
|
679
|
+
category: params.category,
|
|
680
|
+
tool: params.tool,
|
|
681
|
+
packageName: params.packageName,
|
|
682
|
+
tags: params.tags,
|
|
683
|
+
}
|
|
684
|
+
: undefined,
|
|
685
|
+
});
|
|
686
|
+
if (action === "stats") {
|
|
687
|
+
return textResult(formatLifecycleStats(snapshot), {
|
|
688
|
+
action,
|
|
689
|
+
storePath,
|
|
690
|
+
reviewPath,
|
|
691
|
+
curationPath,
|
|
692
|
+
lifecycle: snapshot,
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
const format = params.exportFormat || "markdown";
|
|
696
|
+
return textResult(
|
|
697
|
+
format === "json" ? formatExportJson(snapshot) : formatExportMarkdown(snapshot),
|
|
698
|
+
{
|
|
699
|
+
action,
|
|
700
|
+
exportFormat: format,
|
|
701
|
+
storePath,
|
|
702
|
+
reviewPath,
|
|
703
|
+
curationPath,
|
|
704
|
+
lifecycle: snapshot,
|
|
705
|
+
},
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
const summary = summarizeRecords(records, {
|
|
710
|
+
limit: clampLimit(params.limit, 20),
|
|
711
|
+
curationEvents,
|
|
712
|
+
});
|
|
713
|
+
return textResult(formatSummary(summary), {
|
|
714
|
+
action,
|
|
715
|
+
storePath,
|
|
716
|
+
curationPath,
|
|
717
|
+
summary,
|
|
718
|
+
malformedLines,
|
|
719
|
+
malformedCurationLines,
|
|
720
|
+
});
|
|
721
|
+
},
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
registerAgentVentCommand(
|
|
725
|
+
pi,
|
|
726
|
+
"agent_vent",
|
|
727
|
+
"Inspect local agent vent records: /agent_vent [help|summary|list|facets|review|outcomes|compare|curate|draft|retention|stats|export|path]",
|
|
728
|
+
);
|
|
729
|
+
registerAgentVentCommand(
|
|
730
|
+
pi,
|
|
731
|
+
"agent-vent",
|
|
732
|
+
"Alias for /agent_vent [help|summary|list|facets|review|outcomes|compare|curate|draft|retention|stats|export|path]",
|
|
733
|
+
);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function registerAgentVentCommand(pi: ExtensionAPI, name: string, description: string) {
|
|
737
|
+
pi.registerCommand(name, {
|
|
738
|
+
description,
|
|
739
|
+
handler: async (args, ctx) => {
|
|
740
|
+
const output = handleCommand(args);
|
|
741
|
+
if (ctx.hasUI) {
|
|
742
|
+
ctx.ui.notify(output, "info");
|
|
743
|
+
} else {
|
|
744
|
+
console.log(output);
|
|
745
|
+
}
|
|
746
|
+
},
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
function handleCommand(args: string) {
|
|
751
|
+
const tokens = splitCommandArgs(args);
|
|
752
|
+
const action = tokens[0] || "summary";
|
|
753
|
+
const storePath = defaultStorePath();
|
|
754
|
+
const reviewPath = defaultReviewPath();
|
|
755
|
+
const curationPath = defaultCurationPath();
|
|
756
|
+
const retentionPath = defaultRetentionPath();
|
|
757
|
+
const backupDir = defaultBackupDir();
|
|
758
|
+
|
|
759
|
+
if (action === "help" || action === "--help" || action === "-h") {
|
|
760
|
+
return [
|
|
761
|
+
"agent_vent commands:",
|
|
762
|
+
" /agent_vent summary Show recurrence groups and candidate incidents.",
|
|
763
|
+
" /agent_vent list [limit] Show recent local vent records.",
|
|
764
|
+
" /agent_vent facets [limit] Show read-only local category/tag/tool/package facets.",
|
|
765
|
+
" /agent_vent review [state|all] [limit] [category=bug] [tag=reload] [tool=pi-reload] [package=tryinget-pi-agent-vent]",
|
|
766
|
+
" Show local recurrence review queue with optional local facet filters.",
|
|
767
|
+
" /agent_vent review show <recurrenceKey> [limit] Show bounded representative local samples.",
|
|
768
|
+
" /agent_vent review set <state> <recurrenceKey> [note] Set local review state for a recurrence group.",
|
|
769
|
+
" /agent_vent outcomes [state|all] [per-state-limit] [category=bug] [tag=reload] [tool=pi-reload] [package=tryinget-pi-agent-vent]",
|
|
770
|
+
" Show read-only local follow-up by review outcome bucket.",
|
|
771
|
+
" /agent_vent compare [per-state-limit] [category=bug] [tag=reload] [tool=pi-reload] [package=tryinget-pi-agent-vent]",
|
|
772
|
+
" Compare local review-state buckets before export, retention planning, or draft-only handoff.",
|
|
773
|
+
" /agent_vent curate merge <sourceKey> <targetKey> [note] Append a local merge projection event.",
|
|
774
|
+
" /agent_vent curate rename <sourceKey> <targetKey> [note] Append a local rename projection event.",
|
|
775
|
+
" /agent_vent curate remove <sourceKey> [note] Append a local curation undo event.",
|
|
776
|
+
" /agent_vent draft <github_issue|ak_task|incident_review|maintainer_note> <recurrenceKey> [limit]",
|
|
777
|
+
" Generate draft-only owner-surface text.",
|
|
778
|
+
" /agent_vent retention candidates [reviewed|state|all] [limit] [category=bug] [tag=reload] [tool=pi-reload] [package=tryinget-pi-agent-vent]",
|
|
779
|
+
" Show read-only reviewed groups ready for retention planning, without tokens.",
|
|
780
|
+
" /agent_vent retention history [limit] Show read-only local archive/restore receipt history and restore candidates.",
|
|
781
|
+
" /agent_vent retention preview <recurrenceKey> Preview exact local records and confirmation token.",
|
|
782
|
+
" /agent_vent retention archive <recurrenceKey> <token> [note]",
|
|
783
|
+
" Archive reviewed local records with a backup receipt.",
|
|
784
|
+
" /agent_vent retention restore <backupPath> <token> [note]",
|
|
785
|
+
" Restore a package-created local retention backup.",
|
|
786
|
+
" /agent_vent stats Show local store counts, sizes, and review-state totals.",
|
|
787
|
+
" /agent_vent export [markdown|json] [state|all] [limit] [category=bug] [tag=reload] [tool=pi-reload] [package=tryinget-pi-agent-vent]",
|
|
788
|
+
" Export a bounded local diagnostic projection, optionally facet-filtered.",
|
|
789
|
+
" /agent_vent path Show local JSONL store paths.",
|
|
790
|
+
" /agent-vent ... Backward-compatible alias.",
|
|
791
|
+
"",
|
|
792
|
+
"LLM tool: agent_vent can record minimized frustration events, local review states, local recurrence curation projections, and read-only review comparisons.",
|
|
793
|
+
"Boundary: local diagnostics only; no AK tasks, GitHub issues, incidents, evidence, telemetry, or ASC/self state are created.",
|
|
794
|
+
].join("\n");
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
if (action === "path") {
|
|
798
|
+
return formatPath(storePath, reviewPath, curationPath, retentionPath, backupDir);
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
if (action === "review" || action === "outcomes") {
|
|
802
|
+
const syntaxError = reviewListSyntaxError(tokens.slice(1), action);
|
|
803
|
+
if (syntaxError) return syntaxError;
|
|
804
|
+
}
|
|
805
|
+
if (action === "compare") {
|
|
806
|
+
const syntaxError = compareSyntaxError(tokens.slice(1));
|
|
807
|
+
if (syntaxError) return syntaxError;
|
|
808
|
+
}
|
|
809
|
+
if (action === "export") {
|
|
810
|
+
const syntaxError = exportSyntaxError(tokens.slice(1));
|
|
811
|
+
if (syntaxError) return syntaxError;
|
|
812
|
+
}
|
|
813
|
+
if (action === "retention" && tokens[1] === "candidates") {
|
|
814
|
+
const syntaxError = retentionCandidatesSyntaxError(tokens.slice(2));
|
|
815
|
+
if (syntaxError) return syntaxError;
|
|
816
|
+
}
|
|
817
|
+
if (action === "retention" && tokens[1] === "history") {
|
|
818
|
+
const syntaxError = retentionHistorySyntaxError(tokens.slice(2));
|
|
819
|
+
if (syntaxError) return syntaxError;
|
|
820
|
+
const retentions = readRetentionEvents(retentionPath);
|
|
821
|
+
const history = buildRetentionHistory({
|
|
822
|
+
retentionEvents: retentions.events,
|
|
823
|
+
backupDir,
|
|
824
|
+
limit: clampLimit(tokens[2], 20),
|
|
825
|
+
});
|
|
826
|
+
return `${formatRetentionHistory(history)}${formatRetentionReadWarnings(retentions)}`;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
const state = loadDiagnosticState({
|
|
830
|
+
storePath,
|
|
831
|
+
reviewPath,
|
|
832
|
+
curationPath,
|
|
833
|
+
retentionPath,
|
|
834
|
+
backupDir,
|
|
835
|
+
});
|
|
836
|
+
const { records } = state;
|
|
837
|
+
const suffix = formatDiagnosticWarnings(state);
|
|
838
|
+
|
|
839
|
+
if (action === "review") {
|
|
840
|
+
return `${handleReviewCommand(tokens.slice(1), state, reviewPath)}${suffix}`;
|
|
841
|
+
}
|
|
842
|
+
if (action === "outcomes") {
|
|
843
|
+
return `${handleOutcomesCommand(tokens.slice(1), state)}${suffix}`;
|
|
844
|
+
}
|
|
845
|
+
if (action === "compare") {
|
|
846
|
+
return `${handleCompareCommand(tokens.slice(1), state)}${suffix}`;
|
|
847
|
+
}
|
|
848
|
+
if (action === "curate") {
|
|
849
|
+
return `${handleCurateCommand(tokens.slice(1), state, curationPath)}${suffix}`;
|
|
850
|
+
}
|
|
851
|
+
if (action === "draft") {
|
|
852
|
+
return `${handleDraftCommand(tokens.slice(1), state)}${suffix}`;
|
|
853
|
+
}
|
|
854
|
+
if (action === "retention") {
|
|
855
|
+
return `${handleRetentionCommand(tokens.slice(1), state, {
|
|
856
|
+
storePath,
|
|
857
|
+
reviewPath,
|
|
858
|
+
curationPath,
|
|
859
|
+
retentionPath,
|
|
860
|
+
backupDir,
|
|
861
|
+
})}${suffix}`;
|
|
862
|
+
}
|
|
863
|
+
if (action === "stats" || action === "export") {
|
|
864
|
+
return handleLifecycleCommand(action, tokens.slice(1), state);
|
|
865
|
+
}
|
|
866
|
+
if (action === "facets") {
|
|
867
|
+
return `${formatFacetSummary(
|
|
868
|
+
buildFacetSummary({
|
|
869
|
+
records: state.records as unknown[],
|
|
870
|
+
reviewEvents: state.reviewEvents as unknown[],
|
|
871
|
+
curationEvents: state.curationEvents as unknown[],
|
|
872
|
+
limit: clampLimit(tokens[1], 10),
|
|
873
|
+
}),
|
|
874
|
+
)}${suffix}`;
|
|
875
|
+
}
|
|
876
|
+
if (action === "list") {
|
|
877
|
+
return `${formatRecent(records, clampLimit(tokens[1]))}${suffix}`;
|
|
878
|
+
}
|
|
879
|
+
if (action === "summary") {
|
|
880
|
+
return `${formatSummary(
|
|
881
|
+
summarizeRecords(records, { limit: 20, curationEvents: state.curationEvents }),
|
|
882
|
+
)}${suffix}`;
|
|
883
|
+
}
|
|
884
|
+
return `Unknown /agent_vent action: ${action}\nRun /agent_vent help for usage.`;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
function handleReviewCommand(
|
|
888
|
+
tokens: string[],
|
|
889
|
+
diagnosticState: Record<string, unknown>,
|
|
890
|
+
reviewPath: string,
|
|
891
|
+
) {
|
|
892
|
+
const records = diagnosticState.records as unknown[];
|
|
893
|
+
const curationEvents = diagnosticState.curationEvents as unknown[];
|
|
894
|
+
const reviewEvents = diagnosticState.reviewEvents as unknown[];
|
|
895
|
+
if (tokens[0] === "show") {
|
|
896
|
+
const recurrenceKey = tokens[1];
|
|
897
|
+
const rawLimit = tokens[2];
|
|
898
|
+
if (!recurrenceKey) {
|
|
899
|
+
return "Usage: /agent_vent review show <recurrenceKey> [limit]";
|
|
900
|
+
}
|
|
901
|
+
return formatReviewDetail(
|
|
902
|
+
buildReviewDetail({
|
|
903
|
+
recurrenceKey,
|
|
904
|
+
records,
|
|
905
|
+
reviewEvents,
|
|
906
|
+
curationEvents,
|
|
907
|
+
limit: clampLimit(rawLimit, 5),
|
|
908
|
+
}),
|
|
909
|
+
);
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
if (tokens[0] === "set") {
|
|
913
|
+
const state = tokens[1];
|
|
914
|
+
const recurrenceKey = tokens[2];
|
|
915
|
+
const note = tokens.slice(3).join(" ");
|
|
916
|
+
if (!state || !recurrenceKey) {
|
|
917
|
+
return "Usage: /agent_vent review set <new|acknowledged|dismissed|escalation_drafted> <recurrenceKey> [note]";
|
|
918
|
+
}
|
|
919
|
+
const resolved = resolveRecurrenceGroup(records, recurrenceKey, curationEvents);
|
|
920
|
+
if (!resolved) {
|
|
921
|
+
return `Cannot set review state for unknown recurrence group: ${recurrenceKey}`;
|
|
922
|
+
}
|
|
923
|
+
const event = createReviewEvent(
|
|
924
|
+
{ recurrenceKey: resolved.recurrenceKey, state, note },
|
|
925
|
+
{ source: "agent_vent_command" },
|
|
926
|
+
);
|
|
927
|
+
appendReviewEvent(reviewPath, event);
|
|
928
|
+
return [
|
|
929
|
+
`Set local review state for ${event.recurrenceKey} to ${event.state}.`,
|
|
930
|
+
"Boundary: local diagnostic review state only; no AK task, GitHub issue, incident, evidence, telemetry, or ASC/self state was created.",
|
|
931
|
+
].join("\n");
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
const parsed = parseReviewListTokens(tokens);
|
|
935
|
+
const syntaxError = reviewListSyntaxError(tokens, "review", parsed);
|
|
936
|
+
if (syntaxError) return syntaxError;
|
|
937
|
+
const normalizedState =
|
|
938
|
+
parsed.state && parsed.state !== "all" ? normalizeReviewState(parsed.state) : parsed.state;
|
|
939
|
+
const queue = summarizeReviewQueue(records, reviewEvents, {
|
|
940
|
+
state: normalizedState,
|
|
941
|
+
limit: clampLimit(parsed.limit, 20),
|
|
942
|
+
curationEvents,
|
|
943
|
+
filters: parsed.filters,
|
|
944
|
+
});
|
|
945
|
+
return formatReviewQueue(queue);
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
function handleOutcomesCommand(tokens: string[], diagnosticState: Record<string, unknown>) {
|
|
949
|
+
const parsed = parseReviewListTokens(tokens);
|
|
950
|
+
const syntaxError = reviewListSyntaxError(tokens, "outcomes", parsed);
|
|
951
|
+
if (syntaxError) return syntaxError;
|
|
952
|
+
const normalizedState =
|
|
953
|
+
parsed.state && parsed.state !== "all" ? normalizeReviewState(parsed.state) : parsed.state;
|
|
954
|
+
return formatReviewOutcomes(
|
|
955
|
+
buildReviewOutcomes({
|
|
956
|
+
records: diagnosticState.records as unknown[],
|
|
957
|
+
reviewEvents: diagnosticState.reviewEvents as unknown[],
|
|
958
|
+
curationEvents: diagnosticState.curationEvents as unknown[],
|
|
959
|
+
state: normalizedState || "all",
|
|
960
|
+
limit: clampLimit(parsed.limit, 5),
|
|
961
|
+
filters: parsed.filters,
|
|
962
|
+
}),
|
|
963
|
+
);
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
function handleCompareCommand(tokens: string[], diagnosticState: Record<string, unknown>) {
|
|
967
|
+
const parsed = parseReviewListTokens(tokens);
|
|
968
|
+
const syntaxError = compareSyntaxError(tokens, parsed);
|
|
969
|
+
if (syntaxError) return syntaxError;
|
|
970
|
+
return formatReviewComparison(
|
|
971
|
+
buildReviewComparison({
|
|
972
|
+
records: diagnosticState.records as unknown[],
|
|
973
|
+
reviewEvents: diagnosticState.reviewEvents as unknown[],
|
|
974
|
+
curationEvents: diagnosticState.curationEvents as unknown[],
|
|
975
|
+
limit: clampLimit(parsed.limit, 5),
|
|
976
|
+
filters: parsed.filters,
|
|
977
|
+
}),
|
|
978
|
+
);
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
function reviewListSyntaxError(
|
|
982
|
+
tokens: string[],
|
|
983
|
+
action: "review" | "outcomes",
|
|
984
|
+
parsed = parseReviewListTokens(tokens),
|
|
985
|
+
) {
|
|
986
|
+
if (action === "review" && tokens[0] === "show") {
|
|
987
|
+
if (!tokens[1]) return "Usage: /agent_vent review show <recurrenceKey> [limit]";
|
|
988
|
+
return undefined;
|
|
989
|
+
}
|
|
990
|
+
if (action === "review" && tokens[0] === "set") {
|
|
991
|
+
const state = tokens[1]?.toLowerCase().replaceAll("-", "_");
|
|
992
|
+
if (!tokens[1] || !tokens[2]) {
|
|
993
|
+
return "Usage: /agent_vent review set <new|acknowledged|dismissed|escalation_drafted> <recurrenceKey> [note]";
|
|
994
|
+
}
|
|
995
|
+
if (!REVIEW_STATES.includes(state)) {
|
|
996
|
+
return `Invalid /agent_vent review state: ${tokens[1]}\nUsage: /agent_vent review set <new|acknowledged|dismissed|escalation_drafted> <recurrenceKey> [note]`;
|
|
997
|
+
}
|
|
998
|
+
return undefined;
|
|
999
|
+
}
|
|
1000
|
+
const usage =
|
|
1001
|
+
action === "review"
|
|
1002
|
+
? "Usage: /agent_vent review [state|all] [limit] [category=bug] [tag=reload] [tool=pi-reload] [package=tryinget-pi-agent-vent]"
|
|
1003
|
+
: "Usage: /agent_vent outcomes [state|all] [per-state-limit] [category=bug] [tag=reload] [tool=pi-reload] [package=tryinget-pi-agent-vent]";
|
|
1004
|
+
if (parsed.unknownFilters.length) {
|
|
1005
|
+
return `Unknown /agent_vent ${action} filter(s): ${parsed.unknownFilters.join(", ")}\n${usage}`;
|
|
1006
|
+
}
|
|
1007
|
+
if (parsed.invalidFilters.length) {
|
|
1008
|
+
return `Invalid /agent_vent ${action} filter value(s): ${parsed.invalidFilters.join(", ")}\n${usage}`;
|
|
1009
|
+
}
|
|
1010
|
+
if (parsed.invalidState) {
|
|
1011
|
+
return `Invalid /agent_vent ${action} state: ${parsed.invalidState}\n${usage}`;
|
|
1012
|
+
}
|
|
1013
|
+
return undefined;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
function retentionCandidatesSyntaxError(
|
|
1017
|
+
tokens: string[],
|
|
1018
|
+
parsed = parseReviewListTokens(tokens, { allowReviewedState: true }),
|
|
1019
|
+
) {
|
|
1020
|
+
const usage =
|
|
1021
|
+
"Usage: /agent_vent retention candidates [reviewed|new|acknowledged|dismissed|escalation_drafted|all] [limit] [category=bug] [tag=reload] [tool=pi-reload] [package=tryinget-pi-agent-vent]";
|
|
1022
|
+
if (parsed.unknownFilters.length) {
|
|
1023
|
+
return `Unknown /agent_vent retention candidates filter(s): ${parsed.unknownFilters.join(", ")}\n${usage}`;
|
|
1024
|
+
}
|
|
1025
|
+
if (parsed.invalidFilters.length) {
|
|
1026
|
+
return `Invalid /agent_vent retention candidates filter value(s): ${parsed.invalidFilters.join(", ")}\n${usage}`;
|
|
1027
|
+
}
|
|
1028
|
+
if (parsed.invalidState) {
|
|
1029
|
+
return `Invalid /agent_vent retention candidates state: ${parsed.invalidState}\n${usage}`;
|
|
1030
|
+
}
|
|
1031
|
+
return undefined;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
function retentionHistorySyntaxError(tokens: string[]) {
|
|
1035
|
+
const usage = "Usage: /agent_vent retention history [limit]";
|
|
1036
|
+
if (tokens.length > 1 || (tokens[0] && !/^\d+$/.test(tokens[0]))) {
|
|
1037
|
+
return `Invalid /agent_vent retention history argument: ${tokens.join(" ")}\n${usage}`;
|
|
1038
|
+
}
|
|
1039
|
+
return undefined;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
function compareSyntaxError(tokens: string[], parsed = parseReviewListTokens(tokens)) {
|
|
1043
|
+
const usage =
|
|
1044
|
+
"Usage: /agent_vent compare [per-state-limit] [category=bug] [tag=reload] [tool=pi-reload] [package=tryinget-pi-agent-vent]";
|
|
1045
|
+
if (parsed.unknownFilters.length) {
|
|
1046
|
+
return `Unknown /agent_vent compare filter(s): ${parsed.unknownFilters.join(", ")}\n${usage}`;
|
|
1047
|
+
}
|
|
1048
|
+
if (parsed.invalidFilters.length) {
|
|
1049
|
+
return `Invalid /agent_vent compare filter value(s): ${parsed.invalidFilters.join(", ")}\n${usage}`;
|
|
1050
|
+
}
|
|
1051
|
+
if (parsed.state) {
|
|
1052
|
+
return `Invalid /agent_vent compare argument: ${parsed.invalidState || parsed.state}\n${usage}`;
|
|
1053
|
+
}
|
|
1054
|
+
return undefined;
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
function exportSyntaxError(tokens: string[], parsed = parseLifecycleTokens(tokens)) {
|
|
1058
|
+
const usage =
|
|
1059
|
+
"Usage: /agent_vent export [markdown|json] [state|all] [limit] [category=bug] [tag=reload] [tool=pi-reload] [package=tryinget-pi-agent-vent]";
|
|
1060
|
+
if (parsed.unknownFilters.length) {
|
|
1061
|
+
return `Unknown /agent_vent export filter(s): ${parsed.unknownFilters.join(", ")}\n${usage}`;
|
|
1062
|
+
}
|
|
1063
|
+
if (parsed.invalidFilters.length) {
|
|
1064
|
+
return `Invalid /agent_vent export filter value(s): ${parsed.invalidFilters.join(", ")}\n${usage}`;
|
|
1065
|
+
}
|
|
1066
|
+
if (parsed.invalidState) {
|
|
1067
|
+
return `Invalid /agent_vent export state: ${parsed.invalidState}\n${usage}`;
|
|
1068
|
+
}
|
|
1069
|
+
return undefined;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
function parseReviewListTokens(tokens: string[], options: { allowReviewedState?: boolean } = {}) {
|
|
1073
|
+
const filters: { category?: string; tags?: string[]; tool?: string; packageName?: string } = {};
|
|
1074
|
+
const tags: string[] = [];
|
|
1075
|
+
const unknownFilters: string[] = [];
|
|
1076
|
+
const invalidFilters: string[] = [];
|
|
1077
|
+
let state: string | undefined;
|
|
1078
|
+
let limit: string | undefined;
|
|
1079
|
+
for (const token of tokens) {
|
|
1080
|
+
const separatorIndex = token.indexOf("=");
|
|
1081
|
+
if (separatorIndex > 0) {
|
|
1082
|
+
const key = token.slice(0, separatorIndex).trim().toLowerCase().replaceAll("-", "_");
|
|
1083
|
+
const value = token.slice(separatorIndex + 1).trim();
|
|
1084
|
+
if (key === "category") {
|
|
1085
|
+
if (!value) {
|
|
1086
|
+
invalidFilters.push("category=");
|
|
1087
|
+
continue;
|
|
1088
|
+
}
|
|
1089
|
+
const normalizedCategory = value.toLowerCase().replaceAll("-", "_");
|
|
1090
|
+
if (CATEGORIES.includes(normalizedCategory)) filters.category = normalizedCategory;
|
|
1091
|
+
else invalidFilters.push(`category=${value}`);
|
|
1092
|
+
} else if (key === "tool") {
|
|
1093
|
+
if (!value) {
|
|
1094
|
+
invalidFilters.push("tool=");
|
|
1095
|
+
continue;
|
|
1096
|
+
}
|
|
1097
|
+
filters.tool = value;
|
|
1098
|
+
} else if (key === "package" || key === "package_name" || key === "packagename") {
|
|
1099
|
+
if (!value) {
|
|
1100
|
+
invalidFilters.push(`${key}=`);
|
|
1101
|
+
continue;
|
|
1102
|
+
}
|
|
1103
|
+
filters.packageName = value;
|
|
1104
|
+
} else if (key === "tag" || key === "tags") {
|
|
1105
|
+
if (!value) {
|
|
1106
|
+
invalidFilters.push(`${key}=`);
|
|
1107
|
+
continue;
|
|
1108
|
+
}
|
|
1109
|
+
tags.push(
|
|
1110
|
+
...value
|
|
1111
|
+
.split(",")
|
|
1112
|
+
.map((entry) => entry.trim())
|
|
1113
|
+
.filter(Boolean),
|
|
1114
|
+
);
|
|
1115
|
+
} else {
|
|
1116
|
+
unknownFilters.push(key || token.slice(0, separatorIndex));
|
|
1117
|
+
}
|
|
1118
|
+
continue;
|
|
1119
|
+
}
|
|
1120
|
+
if (!limit && /^\d+$/.test(token)) {
|
|
1121
|
+
limit = token;
|
|
1122
|
+
continue;
|
|
1123
|
+
}
|
|
1124
|
+
if (!state) state = token;
|
|
1125
|
+
}
|
|
1126
|
+
const normalizedState = state?.toLowerCase().replaceAll("-", "_");
|
|
1127
|
+
const invalidState =
|
|
1128
|
+
normalizedState &&
|
|
1129
|
+
normalizedState !== "all" &&
|
|
1130
|
+
!(options.allowReviewedState && normalizedState === "reviewed") &&
|
|
1131
|
+
!REVIEW_STATES.includes(normalizedState)
|
|
1132
|
+
? state
|
|
1133
|
+
: undefined;
|
|
1134
|
+
return {
|
|
1135
|
+
state: normalizedState,
|
|
1136
|
+
limit,
|
|
1137
|
+
filters: tags.length ? { ...filters, tags } : filters,
|
|
1138
|
+
unknownFilters,
|
|
1139
|
+
invalidFilters,
|
|
1140
|
+
invalidState,
|
|
1141
|
+
};
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
function parseLifecycleTokens(tokens: string[]) {
|
|
1145
|
+
const format = tokens[0] === "json" || tokens[0] === "markdown" ? tokens[0] : "markdown";
|
|
1146
|
+
const parsed = parseReviewListTokens(format === tokens[0] ? tokens.slice(1) : tokens);
|
|
1147
|
+
return {
|
|
1148
|
+
format,
|
|
1149
|
+
state: parsed.state,
|
|
1150
|
+
limit: parsed.limit,
|
|
1151
|
+
filters: parsed.filters,
|
|
1152
|
+
unknownFilters: parsed.unknownFilters,
|
|
1153
|
+
invalidFilters: parsed.invalidFilters,
|
|
1154
|
+
invalidState: parsed.invalidState,
|
|
1155
|
+
};
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
function handleCurateCommand(
|
|
1159
|
+
tokens: string[],
|
|
1160
|
+
state: Record<string, unknown>,
|
|
1161
|
+
curationPath: string,
|
|
1162
|
+
) {
|
|
1163
|
+
const records = state.records as unknown[];
|
|
1164
|
+
const curationEvents = state.curationEvents as unknown[];
|
|
1165
|
+
const curationAction = tokens[0];
|
|
1166
|
+
const sourceRecurrenceKey = tokens[1];
|
|
1167
|
+
const targetRecurrenceKey = curationAction === "remove" ? undefined : tokens[2];
|
|
1168
|
+
const note = tokens.slice(curationAction === "remove" ? 2 : 3).join(" ");
|
|
1169
|
+
if (
|
|
1170
|
+
!curationAction ||
|
|
1171
|
+
!sourceRecurrenceKey ||
|
|
1172
|
+
(curationAction !== "remove" && !targetRecurrenceKey)
|
|
1173
|
+
) {
|
|
1174
|
+
return "Usage: /agent_vent curate <merge|rename> <sourceRecurrenceKey> <targetRecurrenceKey> [note] OR /agent_vent curate remove <sourceRecurrenceKey> [note]";
|
|
1175
|
+
}
|
|
1176
|
+
const input = { action: curationAction, sourceRecurrenceKey, targetRecurrenceKey, note };
|
|
1177
|
+
assertCanCurateRecurrence(records, curationEvents, input);
|
|
1178
|
+
const event = createCurationEvent(input, { source: "agent_vent_command" });
|
|
1179
|
+
appendCurationEvent(curationPath, event);
|
|
1180
|
+
const targetText = event.targetRecurrenceKey ? ` -> ${event.targetRecurrenceKey}` : "";
|
|
1181
|
+
return [
|
|
1182
|
+
`Recorded local recurrence ${event.action} curation: ${event.sourceRecurrenceKey}${targetText}.`,
|
|
1183
|
+
"Boundary: local diagnostic curation projection only; raw vents were not rewritten and no AK task, GitHub issue, incident, evidence, telemetry, or ASC/self state was created.",
|
|
1184
|
+
].join("\n");
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
function handleDraftCommand(tokens: string[], state: Record<string, unknown>) {
|
|
1188
|
+
const records = state.records as unknown[];
|
|
1189
|
+
const reviewEvents = state.reviewEvents as unknown[];
|
|
1190
|
+
const curationEvents = state.curationEvents as unknown[];
|
|
1191
|
+
const draftTarget = tokens[0];
|
|
1192
|
+
const recurrenceKey = tokens[1];
|
|
1193
|
+
const rawLimit = tokens[2];
|
|
1194
|
+
if (!draftTarget || !recurrenceKey) {
|
|
1195
|
+
return "Usage: /agent_vent draft <github_issue|ak_task|incident_review|maintainer_note> <recurrenceKey> [limit]";
|
|
1196
|
+
}
|
|
1197
|
+
return buildEscalationDraft({
|
|
1198
|
+
target: draftTarget,
|
|
1199
|
+
recurrenceKey,
|
|
1200
|
+
records,
|
|
1201
|
+
reviewEvents,
|
|
1202
|
+
curationEvents,
|
|
1203
|
+
limit: clampLimit(rawLimit, 5),
|
|
1204
|
+
}).text;
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
function handleRetentionCommand(
|
|
1208
|
+
tokens: string[],
|
|
1209
|
+
state: Record<string, unknown>,
|
|
1210
|
+
paths: {
|
|
1211
|
+
storePath: string;
|
|
1212
|
+
reviewPath: string;
|
|
1213
|
+
curationPath: string;
|
|
1214
|
+
retentionPath: string;
|
|
1215
|
+
backupDir: string;
|
|
1216
|
+
},
|
|
1217
|
+
) {
|
|
1218
|
+
const retentionAction = normalizeRetentionAction(tokens[0] || "preview");
|
|
1219
|
+
if (retentionAction === "candidates") {
|
|
1220
|
+
const parsed = parseReviewListTokens(tokens.slice(1), { allowReviewedState: true });
|
|
1221
|
+
const syntaxError = retentionCandidatesSyntaxError(tokens.slice(1), parsed);
|
|
1222
|
+
if (syntaxError) return syntaxError;
|
|
1223
|
+
return formatRetentionCandidates(
|
|
1224
|
+
buildRetentionCandidates({
|
|
1225
|
+
records: state.records as unknown[],
|
|
1226
|
+
reviewEvents: state.reviewEvents as unknown[],
|
|
1227
|
+
curationEvents: state.curationEvents as unknown[],
|
|
1228
|
+
state: parsed.state,
|
|
1229
|
+
limit: clampLimit(parsed.limit, 20),
|
|
1230
|
+
filters: parsed.filters,
|
|
1231
|
+
}),
|
|
1232
|
+
);
|
|
1233
|
+
}
|
|
1234
|
+
if (retentionAction === "preview") {
|
|
1235
|
+
const recurrenceKey = tokens[1];
|
|
1236
|
+
if (!recurrenceKey) return "Usage: /agent_vent retention preview <recurrenceKey>";
|
|
1237
|
+
return formatRetentionPreview(
|
|
1238
|
+
buildRetentionPreview({
|
|
1239
|
+
recurrenceKey,
|
|
1240
|
+
records: state.records as unknown[],
|
|
1241
|
+
reviewEvents: state.reviewEvents as unknown[],
|
|
1242
|
+
curationEvents: state.curationEvents as unknown[],
|
|
1243
|
+
storeHash: state.ventsHash as string,
|
|
1244
|
+
reviewHash: state.reviewEventsHash as string,
|
|
1245
|
+
curationHash: state.curationEventsHash as string,
|
|
1246
|
+
limit: 5,
|
|
1247
|
+
}),
|
|
1248
|
+
);
|
|
1249
|
+
}
|
|
1250
|
+
if (retentionAction === "archive") {
|
|
1251
|
+
const recurrenceKey = tokens[1];
|
|
1252
|
+
const confirmationToken = tokens[2];
|
|
1253
|
+
const note = tokens.slice(3).join(" ");
|
|
1254
|
+
if (!recurrenceKey || !confirmationToken) {
|
|
1255
|
+
return "Usage: /agent_vent retention archive <recurrenceKey> <confirmationToken> [note]";
|
|
1256
|
+
}
|
|
1257
|
+
return formatRetentionArchiveResult(
|
|
1258
|
+
archiveRecurrenceGroup({
|
|
1259
|
+
...paths,
|
|
1260
|
+
recurrenceKey,
|
|
1261
|
+
confirmationToken,
|
|
1262
|
+
note,
|
|
1263
|
+
source: "agent_vent_command",
|
|
1264
|
+
}),
|
|
1265
|
+
);
|
|
1266
|
+
}
|
|
1267
|
+
const backupPath = tokens[1];
|
|
1268
|
+
const confirmationToken = tokens[2];
|
|
1269
|
+
const note = tokens.slice(3).join(" ");
|
|
1270
|
+
if (!backupPath || !confirmationToken) {
|
|
1271
|
+
return "Usage: /agent_vent retention restore <backupPath> <confirmationToken> [note]";
|
|
1272
|
+
}
|
|
1273
|
+
return formatRetentionRestoreResult(
|
|
1274
|
+
restoreRetentionBackup({
|
|
1275
|
+
storePath: paths.storePath,
|
|
1276
|
+
retentionPath: paths.retentionPath,
|
|
1277
|
+
backupDir: paths.backupDir,
|
|
1278
|
+
backupPath,
|
|
1279
|
+
confirmationToken,
|
|
1280
|
+
note,
|
|
1281
|
+
source: "agent_vent_command",
|
|
1282
|
+
}),
|
|
1283
|
+
);
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
function splitCommandArgs(args: string) {
|
|
1287
|
+
const tokens: string[] = [];
|
|
1288
|
+
let current = "";
|
|
1289
|
+
let quote: string | undefined;
|
|
1290
|
+
let escaping = false;
|
|
1291
|
+
for (const char of args.trim()) {
|
|
1292
|
+
if (escaping) {
|
|
1293
|
+
current += char;
|
|
1294
|
+
escaping = false;
|
|
1295
|
+
continue;
|
|
1296
|
+
}
|
|
1297
|
+
if (char === "\\") {
|
|
1298
|
+
escaping = true;
|
|
1299
|
+
continue;
|
|
1300
|
+
}
|
|
1301
|
+
if (quote) {
|
|
1302
|
+
if (char === quote) quote = undefined;
|
|
1303
|
+
else current += char;
|
|
1304
|
+
continue;
|
|
1305
|
+
}
|
|
1306
|
+
if (char === '"' || char === "'") {
|
|
1307
|
+
quote = char;
|
|
1308
|
+
continue;
|
|
1309
|
+
}
|
|
1310
|
+
if (/\s/.test(char)) {
|
|
1311
|
+
if (current) {
|
|
1312
|
+
tokens.push(current);
|
|
1313
|
+
current = "";
|
|
1314
|
+
}
|
|
1315
|
+
continue;
|
|
1316
|
+
}
|
|
1317
|
+
current += char;
|
|
1318
|
+
}
|
|
1319
|
+
if (escaping) current += "\\";
|
|
1320
|
+
if (current) tokens.push(current);
|
|
1321
|
+
return tokens;
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
function handleLifecycleCommand(action: string, tokens: string[], state: Record<string, unknown>) {
|
|
1325
|
+
const parsed = parseLifecycleTokens(tokens);
|
|
1326
|
+
const snapshot = buildLifecycleSnapshot({
|
|
1327
|
+
...state,
|
|
1328
|
+
state: parsed.state || "all",
|
|
1329
|
+
limit: clampLimit(parsed.limit, 20),
|
|
1330
|
+
filters: action === "export" ? parsed.filters : undefined,
|
|
1331
|
+
});
|
|
1332
|
+
|
|
1333
|
+
if (action === "stats") return formatLifecycleStats(snapshot);
|
|
1334
|
+
return parsed.format === "json" ? formatExportJson(snapshot) : formatExportMarkdown(snapshot);
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
function formatRetentionReadWarnings(retentions: {
|
|
1338
|
+
malformedLines?: number;
|
|
1339
|
+
oversizedLines?: number;
|
|
1340
|
+
invalidEvents?: number;
|
|
1341
|
+
}) {
|
|
1342
|
+
const warnings = [];
|
|
1343
|
+
if (Number(retentions.malformedLines) > 0)
|
|
1344
|
+
warnings.push(`malformed retention=${retentions.malformedLines}`);
|
|
1345
|
+
if (Number(retentions.oversizedLines) > 0)
|
|
1346
|
+
warnings.push(`oversized retention=${retentions.oversizedLines}`);
|
|
1347
|
+
if (Number(retentions.invalidEvents) > 0)
|
|
1348
|
+
warnings.push(`invalid retention=${retentions.invalidEvents}`);
|
|
1349
|
+
return warnings.length > 0
|
|
1350
|
+
? `\nWarning: ignored local retention receipt entries (${warnings.join(", ")}).`
|
|
1351
|
+
: "";
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
function formatDiagnosticWarnings(state: Record<string, unknown>) {
|
|
1355
|
+
const warnings = [];
|
|
1356
|
+
const pairs = [
|
|
1357
|
+
["malformed vent", state.malformedLines],
|
|
1358
|
+
["malformed review", state.malformedReviewLines],
|
|
1359
|
+
["malformed curation", state.malformedCurationLines],
|
|
1360
|
+
["malformed retention", state.malformedRetentionLines],
|
|
1361
|
+
["oversized vent", state.oversizedLines],
|
|
1362
|
+
["oversized review", state.oversizedReviewLines],
|
|
1363
|
+
["oversized curation", state.oversizedCurationLines],
|
|
1364
|
+
["oversized retention", state.oversizedRetentionLines],
|
|
1365
|
+
["invalid vent", state.invalidRecords],
|
|
1366
|
+
["invalid review", state.invalidReviewEvents],
|
|
1367
|
+
["invalid curation", state.invalidCurationEvents],
|
|
1368
|
+
["invalid retention", state.invalidRetentionEvents],
|
|
1369
|
+
["quarantined curation", state.quarantinedCurationEvents],
|
|
1370
|
+
];
|
|
1371
|
+
for (const [label, value] of pairs) {
|
|
1372
|
+
if (Number(value) > 0) warnings.push(`${label}=${value}`);
|
|
1373
|
+
}
|
|
1374
|
+
return warnings.length > 0
|
|
1375
|
+
? `\nWarning: ignored local diagnostic entries (${warnings.join(", ")}).`
|
|
1376
|
+
: "";
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
function textResult(text: string, details: Record<string, unknown>) {
|
|
1380
|
+
return {
|
|
1381
|
+
content: [{ type: "text" as const, text }],
|
|
1382
|
+
details,
|
|
1383
|
+
};
|
|
1384
|
+
}
|