pi-advisor-flow 0.2.3 → 0.2.6

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.
@@ -0,0 +1,50 @@
1
+ import { lstat, open, realpath } from "node:fs/promises";
2
+ import { join, relative } from "node:path";
3
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
4
+ import { redactAndCapText } from "./conversation.js";
5
+
6
+ export const PREFERENCES_MAX_BYTES = 8 * 1024;
7
+ export interface TextAttachment {
8
+ bytes: number;
9
+ text: string;
10
+ }
11
+
12
+ const inside = (root: string, candidate: string) => {
13
+ const path = relative(root, candidate);
14
+ return path === "" || !(path.startsWith("..") || path.includes("../"));
15
+ };
16
+
17
+ /** Reads trusted project preferences without following a file or root escape. */
18
+ export const readProjectPreferences = async (
19
+ ctx: ExtensionContext,
20
+ maxBytes = PREFERENCES_MAX_BYTES,
21
+ redact = true
22
+ ): Promise<TextAttachment | undefined> => {
23
+ if (!ctx.isProjectTrusted()) {
24
+ return;
25
+ }
26
+ try {
27
+ const root = await realpath(ctx.cwd);
28
+ const candidate = join(ctx.cwd, ".pi", "advisor-preferences.md");
29
+ const stats = await lstat(candidate);
30
+ if (stats.isSymbolicLink() || !stats.isFile()) {
31
+ return;
32
+ }
33
+ const resolved = await realpath(candidate);
34
+ if (!inside(root, resolved)) {
35
+ return;
36
+ }
37
+ const file = await open(resolved, "r");
38
+ try {
39
+ const buffer = Buffer.alloc(maxBytes + 1);
40
+ const { bytesRead } = await file.read(buffer, 0, buffer.length, 0);
41
+ const source = buffer.subarray(0, bytesRead).toString("utf8");
42
+ const capped = redactAndCapText(source, maxBytes, redact);
43
+ return { bytes: Buffer.byteLength(capped, "utf8"), text: capped };
44
+ } finally {
45
+ await file.close();
46
+ }
47
+ } catch {
48
+ // Missing, unreadable, or unsafe preferences are intentionally withheld.
49
+ }
50
+ };
@@ -112,6 +112,13 @@ export class AdvisorSessionState {
112
112
  #invocations: AdvisorInvocationRecord[] = [];
113
113
  #loopInterventions = 0;
114
114
  #consumedCalls = 0;
115
+ readonly #issuedAdvice = new Map<
116
+ string,
117
+ { advice: string; trigger: ConsultationTrigger }
118
+ >();
119
+ readonly #reportedAdvice = new Set<string>();
120
+ #draftConsultations = 0;
121
+ #outcomes = 0;
115
122
 
116
123
  resetTask() {
117
124
  this.#previousSignature = undefined;
@@ -120,6 +127,10 @@ export class AdvisorSessionState {
120
127
  this.#invocations = [];
121
128
  this.#loopInterventions = 0;
122
129
  this.#consumedCalls = 0;
130
+ this.#issuedAdvice.clear();
131
+ this.#reportedAdvice.clear();
132
+ this.#draftConsultations = 0;
133
+ this.#outcomes = 0;
123
134
  }
124
135
 
125
136
  clearBlocked() {
@@ -172,6 +183,29 @@ export class AdvisorSessionState {
172
183
  recordInvocation(record: AdvisorInvocationRecord) {
173
184
  this.#invocations.push(record);
174
185
  }
186
+ issueAdvice(
187
+ id: string,
188
+ advice: string,
189
+ trigger: ConsultationTrigger,
190
+ draft = false
191
+ ) {
192
+ this.#issuedAdvice.set(id, { advice, trigger });
193
+ if (draft) {
194
+ this.#draftConsultations += 1;
195
+ }
196
+ }
197
+ claimAdvice(id: string) {
198
+ if (this.#reportedAdvice.has(id)) {
199
+ return;
200
+ }
201
+ const advice = this.#issuedAdvice.get(id);
202
+ if (!advice) {
203
+ return;
204
+ }
205
+ this.#reportedAdvice.add(id);
206
+ this.#outcomes += 1;
207
+ return advice;
208
+ }
175
209
 
176
210
  summary(limit: number | undefined) {
177
211
  if (this.#invocations.length === 0 && this.#loopInterventions === 0) {
@@ -215,7 +249,8 @@ export class AdvisorSessionState {
215
249
  `Triggers: ${["manual", "executor-requested", "repeated-tool-call", "completion-review", "custom-rule"].filter((trigger) => countTrigger(trigger as AdvisorTrigger) > 0).join(", ") || "none"}`,
216
250
  `Models: ${models}`,
217
251
  `Budget: ${budget}`,
218
- `Markdown advice: ${markdown.length} responses`,
252
+ `Markdown advice: ${markdown.length} responses (${this.#draftConsultations} with drafts)`,
253
+ `Outcome reports: ${this.#outcomes}`,
219
254
  `Gate decisions: ${decisions}`,
220
255
  `Loop matching: normalized tool signatures; ${this.#loopInterventions} gate intervention${this.#loopInterventions === 1 ? "" : "s"}`,
221
256
  `Execution effects: ${effects("tool-blocked")} tool blocked, ${effects("session-blocked")} sessions blocked, ${effects("continued")} continued`,