pi-advisor-flow 0.9.2-dev.3 → 0.9.2-dev.5

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.
Files changed (40) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +7 -2
  3. package/dist/index.js +580 -201
  4. package/package.json +1 -1
  5. package/src/commands/manual-consultation.ts +31 -26
  6. package/src/commands/outcome-stats.ts +98 -0
  7. package/src/commands/registration.ts +2 -0
  8. package/src/commands/renderers.ts +51 -0
  9. package/src/commands/runtime.ts +2 -3
  10. package/src/commands/settings-persistence.ts +16 -7
  11. package/src/commands/types.ts +3 -2
  12. package/src/config/args.ts +3 -1
  13. package/src/config/defaults.ts +7 -0
  14. package/src/config/schema.ts +7 -0
  15. package/src/config/state.ts +5 -0
  16. package/src/config/storage.ts +23 -2
  17. package/src/config/types.ts +1 -0
  18. package/src/config.ts +2 -0
  19. package/src/image-attachments.ts +7 -3
  20. package/src/outcome-stats.ts +154 -0
  21. package/src/outcomes.ts +8 -1
  22. package/src/preferences.ts +103 -13
  23. package/src/redaction.ts +1 -1
  24. package/src/tools/consult-context.ts +12 -1
  25. package/src/tools/consultation.ts +34 -7
  26. package/src/tools/jev-filter.ts +2 -1
  27. package/src/tools/jev-turn-gate.ts +2 -0
  28. package/src/tools/loop-gate.ts +2 -0
  29. package/src/tools/prompts.ts +28 -2
  30. package/src/tools/register-ask-advisor.ts +1 -0
  31. package/src/tools/register-renderers.ts +40 -6
  32. package/src/tools/registration.ts +2 -2
  33. package/src/tools/render-advisor-result.ts +21 -11
  34. package/src/tools/session.ts +14 -0
  35. package/src/tools/types.ts +3 -0
  36. package/src/ui/manual-dialog.ts +10 -3
  37. package/src/ui/settings-items.ts +7 -0
  38. package/src/ui/settings-mutations.ts +1 -0
  39. package/src/ui/types.ts +1 -0
  40. package/src/usage.ts +34 -6
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-advisor-flow",
3
- "version": "0.9.2-dev.3",
3
+ "version": "0.9.2-dev.5",
4
4
  "description": "Advanced Executor/Advisor flow for Pi, fully configurable and extendable.",
5
5
  "keywords": [
6
6
  "advisor",
@@ -15,6 +15,8 @@ import type { CommandRuntime, ManualAdvisorProgressState } from "./types.ts";
15
15
 
16
16
  interface ManualResultDetails {
17
17
  advisor: string;
18
+ agentRulesBytes?: number;
19
+ preferenceBytes?: number;
18
20
  question?: string;
19
21
  text: string;
20
22
  usage?: AdvisorUsageSnapshot;
@@ -48,34 +50,35 @@ export const startManualConsultation = async (
48
50
  const finishHerdrActivity = runtime.herdrActivity.start();
49
51
  let scoutDetails: ScoutToolDetails | undefined;
50
52
  try {
51
- const { adviceId, markdown, usage } = await runtime.requestAdvisor(
52
- ctx,
53
- question,
54
- controller.signal,
55
- (thinking, text) => {
56
- if (controller.signal.aborted) {
57
- return;
58
- }
59
- progress.phase = "active";
60
- progress.thinking = thinking;
61
- progress.text = text;
62
- runtime.requestManualRender(ctx);
63
- },
64
- (event) => {
65
- if (!controller.signal.aborted) {
66
- runtime.scoutStatus.update(ctx, scoutStatusToken, event);
67
- scoutDetails = appendScoutLifecycleEntry(
68
- runtime.pi,
69
- event,
70
- scoutDetails
71
- );
72
- progress.scout = scoutDetails;
53
+ const { adviceId, agentRulesBytes, markdown, preferenceBytes, usage } =
54
+ await runtime.requestAdvisor(
55
+ ctx,
56
+ question,
57
+ controller.signal,
58
+ (thinking, text) => {
59
+ if (controller.signal.aborted) {
60
+ return;
61
+ }
73
62
  progress.phase = "active";
63
+ progress.thinking = thinking;
64
+ progress.text = text;
74
65
  runtime.requestManualRender(ctx);
75
- }
76
- },
77
- gitContext
78
- );
66
+ },
67
+ (event) => {
68
+ if (!controller.signal.aborted) {
69
+ runtime.scoutStatus.update(ctx, scoutStatusToken, event);
70
+ scoutDetails = appendScoutLifecycleEntry(
71
+ runtime.pi,
72
+ event,
73
+ scoutDetails
74
+ );
75
+ progress.scout = scoutDetails;
76
+ progress.phase = "active";
77
+ runtime.requestManualRender(ctx);
78
+ }
79
+ },
80
+ gitContext
81
+ );
79
82
  if (controller.signal.aborted) {
80
83
  return;
81
84
  }
@@ -103,6 +106,8 @@ export const startManualConsultation = async (
103
106
  runtime.updateAdvisorUsageStatus(ctx);
104
107
  const details: ManualResultDetails = {
105
108
  advisor: advisorRef,
109
+ agentRulesBytes,
110
+ preferenceBytes,
106
111
  question,
107
112
  text: markdown,
108
113
  };
@@ -0,0 +1,98 @@
1
+ import { aggregateOutcomeRecords, readOutcomeLog } from "../outcome-stats.ts";
2
+ import type { OutcomeStats } from "../outcome-stats.ts";
3
+ import { OUTCOME_LOG_MAX_BYTES } from "../outcomes.ts";
4
+ import type { CommandRuntime } from "./types.ts";
5
+
6
+ export interface AdvisorStatsDetails {
7
+ error?: string;
8
+ malformedLines: number;
9
+ stats?: OutcomeStats;
10
+ }
11
+
12
+ const percent = (rate: number | undefined) =>
13
+ rate === undefined ? "n/a" : `${(rate * 100).toFixed(1)}%`;
14
+
15
+ const validatedCount = (value: { failed: number; passed: number }) =>
16
+ value.failed + value.passed;
17
+
18
+ const timestamp = (value: string | undefined) =>
19
+ value ? new Date(value).toLocaleString() : "n/a";
20
+
21
+ export const formatAdvisorStats = ({
22
+ error,
23
+ malformedLines,
24
+ stats,
25
+ }: AdvisorStatsDetails): string => {
26
+ if (error) {
27
+ return `**Could not read Advisor outcomes.**\n\n${error}`;
28
+ }
29
+ if (!stats || stats.total === 0) {
30
+ const ignored = malformedLines
31
+ ? `\n\nIgnored ${malformedLines} malformed ledger line${malformedLines === 1 ? "" : "s"}.`
32
+ : "";
33
+ return `**No Advisor outcomes recorded yet.**\n\nEnable outcome logging and record an outcome after an Advisor response to build this report.${ignored}`;
34
+ }
35
+ const { validationByAdoption } = stats;
36
+ const { followed } = validationByAdoption;
37
+ const notFollowed = validationByAdoption["not-followed"];
38
+ const triggerLines = Object.entries(stats.byTrigger).map(
39
+ ([trigger, count]) => `- ${trigger}: ${count}`
40
+ );
41
+ const adoptionLines = Object.entries(stats.adoption).map(
42
+ ([adoption, count]) => `- ${adoption}: ${count}`
43
+ );
44
+ const validationLines = [
45
+ `- followed: ${percent(followed.passRate)} passed (${followed.passed}/${validatedCount(followed)})`,
46
+ `- not-followed: ${percent(notFollowed.passRate)} passed (${notFollowed.passed}/${validatedCount(notFollowed)})`,
47
+ ];
48
+ const ignored = malformedLines
49
+ ? `\n\nIgnored ${malformedLines} malformed ledger line${malformedLines === 1 ? "" : "s"}.`
50
+ : "";
51
+ return [
52
+ `**${stats.total} Advisor outcome${stats.total === 1 ? "" : "s"}**`,
53
+ "",
54
+ "**By trigger**",
55
+ ...triggerLines,
56
+ "",
57
+ "**Adoption**",
58
+ ...adoptionLines,
59
+ "",
60
+ "**Validation passed rate (passed / passed + failed)**",
61
+ ...validationLines,
62
+ "",
63
+ `Distinct advices seen: ${stats.distinctAdvices}`,
64
+ `Time window: ${timestamp(stats.firstTimestamp)} → ${timestamp(stats.lastTimestamp)}`,
65
+ `Ledger window: capped at ${OUTCOME_LOG_MAX_BYTES / (1024 * 1024)} MiB and rewritten on overflow; this report covers only retained records.${ignored}`,
66
+ ].join("\n");
67
+ };
68
+
69
+ const readAdvisorStats = async (): Promise<AdvisorStatsDetails> => {
70
+ try {
71
+ const log = await readOutcomeLog();
72
+ const stats = aggregateOutcomeRecords(log.records, log.malformedLines);
73
+ return { malformedLines: log.malformedLines, stats };
74
+ } catch (error) {
75
+ return {
76
+ error: error instanceof Error ? error.message : String(error),
77
+ malformedLines: 0,
78
+ };
79
+ }
80
+ };
81
+
82
+ export const registerOutcomeStatsCommand = (runtime: CommandRuntime) => {
83
+ runtime.pi.registerCommand("advisor-stats", {
84
+ description: "Show retained Advisor outcome adoption and validation stats",
85
+ handler: async (_args, _ctx) => {
86
+ const details = await readAdvisorStats();
87
+ runtime.pi.sendMessage(
88
+ {
89
+ content: [],
90
+ customType: "advisor-stats-result",
91
+ details,
92
+ display: true,
93
+ },
94
+ { deliverAs: "steer", triggerTurn: false }
95
+ );
96
+ },
97
+ });
98
+ };
@@ -7,6 +7,7 @@ import { activateAdvisor } from "./activation.ts";
7
7
  import { registerCommandLifecycle } from "./lifecycle.ts";
8
8
  import { registerManualCommand } from "./manual-command.ts";
9
9
  import { registerModelCommands } from "./model-commands.ts";
10
+ import { registerOutcomeStatsCommand } from "./outcome-stats.ts";
10
11
  import { registerCommandRenderers } from "./renderers.ts";
11
12
  import { createCommandRuntime } from "./runtime.ts";
12
13
  import { registerSettingsCommands } from "./settings-commands.ts";
@@ -24,5 +25,6 @@ export const registerCommands = (
24
25
  registerCommandLifecycle(runtime, activate);
25
26
  registerManualCommand(runtime);
26
27
  registerModelCommands(runtime);
28
+ registerOutcomeStatsCommand(runtime);
27
29
  registerSettingsCommands(runtime);
28
30
  };
@@ -15,6 +15,8 @@ import {
15
15
  } from "../tools/render-common.ts";
16
16
  import { formatAdvisorUsage } from "../usage.ts";
17
17
  import { ManualAdvisorProgressComponent } from "./manual-progress.ts";
18
+ import { formatAdvisorStats } from "./outcome-stats.ts";
19
+ import type { AdvisorStatsDetails } from "./outcome-stats.ts";
18
20
  import type { CommandRuntime } from "./types.ts";
19
21
 
20
22
  interface ManualCallEntryDetails {
@@ -24,10 +26,22 @@ interface ManualCallEntryDetails {
24
26
 
25
27
  interface ManualResultDetails {
26
28
  advisor?: string;
29
+ agentRulesBytes?: number;
30
+ preferenceBytes?: number;
27
31
  text?: string;
28
32
  usage?: unknown;
29
33
  }
30
34
 
35
+ const formatByteSize = (bytes: number) => {
36
+ if (bytes >= 1024 * 1024) {
37
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MiB`;
38
+ }
39
+ if (bytes >= 1024) {
40
+ return `${Math.round(bytes / 1024)} KiB`;
41
+ }
42
+ return `${bytes} B`;
43
+ };
44
+
31
45
  const manualCallRenderer =
32
46
  (runtime: CommandRuntime): EntryRenderer<ManualCallEntryDetails> =>
33
47
  (entry, { expanded }, theme) => {
@@ -70,12 +84,45 @@ const manualResultRenderer: MessageRenderer<ManualResultDetails> = (
70
84
  box.addChild(new Text(theme.fg("dim", ` Usage: ${usage}`), 0, 0));
71
85
  }
72
86
  }
87
+ const attachments = [
88
+ details?.agentRulesBytes
89
+ ? `AGENTS.md rules attached · ${formatByteSize(details.agentRulesBytes)}`
90
+ : undefined,
91
+ details?.preferenceBytes
92
+ ? `Project preferences attached · ${formatByteSize(details.preferenceBytes)}`
93
+ : undefined,
94
+ ].filter((value): value is string => value !== undefined);
95
+ if (attachments.length) {
96
+ box.addChild(
97
+ new Text(theme.fg("dim", ` ${attachments.join(" · ")}`), 0, 0)
98
+ );
99
+ }
73
100
  box.addChild(
74
101
  new Markdown(adviceForDisplay(advice, expanded), 0, 0, getMarkdownTheme())
75
102
  );
76
103
  return box;
77
104
  };
78
105
 
106
+ const outcomeStatsRenderer: MessageRenderer<AdvisorStatsDetails> = (
107
+ message,
108
+ _options,
109
+ theme
110
+ ) => {
111
+ const box = new Box(1, 1, (text) => theme.bg("customMessageBg", text));
112
+ box.addChild(
113
+ new Text(theme.fg("accent", theme.bold("◆ ADVISOR · OUTCOME STATS")), 0, 0)
114
+ );
115
+ box.addChild(
116
+ new Markdown(
117
+ formatAdvisorStats(message.details ?? { malformedLines: 0 }),
118
+ 0,
119
+ 0,
120
+ getMarkdownTheme()
121
+ )
122
+ );
123
+ return box;
124
+ };
125
+
79
126
  export const registerCommandRenderers = (runtime: CommandRuntime) => {
80
127
  runtime.pi.registerEntryRenderer?.(
81
128
  "advisor-manual-call",
@@ -85,4 +132,8 @@ export const registerCommandRenderers = (runtime: CommandRuntime) => {
85
132
  "advisor-manual-result",
86
133
  manualResultRenderer
87
134
  );
135
+ runtime.pi.registerMessageRenderer?.(
136
+ "advisor-stats-result",
137
+ outcomeStatsRenderer
138
+ );
88
139
  };
@@ -12,7 +12,7 @@ import {
12
12
  sameModelAdvisorNotice,
13
13
  } from "../tools/model-access.ts";
14
14
  import { ScoutStatusManager } from "../tools/scout-status.ts";
15
- import { advisorSessionState as defaultAdvisorSessionState } from "../tools/session.ts";
15
+ import { sessionStateFor } from "../tools/session.ts";
16
16
  import { uiAction } from "../ui-guard.ts";
17
17
  import type {
18
18
  CommandDependencies,
@@ -60,8 +60,7 @@ class CommandRuntime implements CommandRuntimeContract {
60
60
 
61
61
  constructor(pi: ExtensionAPI, dependencies: CommandDependencies = {}) {
62
62
  this.pi = pi;
63
- this.advisorSessionState =
64
- dependencies.sessionState ?? defaultAdvisorSessionState;
63
+ this.advisorSessionState = dependencies.sessionState ?? sessionStateFor(pi);
65
64
  this.herdrActivity = dependencies.herdrActivity ?? herdrAdvisorActivity;
66
65
  this.scoutStatus =
67
66
  dependencies.statusManager ?? new ScoutStatusManager(false);
@@ -1,7 +1,9 @@
1
1
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
 
3
3
  import {
4
+ getAdvisorSettings,
4
5
  getPersistedModelRefs,
6
+ setAdvisorAgentsMdContextRef,
5
7
  setAdvisorAutoLoopGateRef,
6
8
  setAdvisorBlockOnBlockedRef,
7
9
  setAdvisorCollapseResponsesRef,
@@ -125,6 +127,7 @@ const applyDisclosureSettings = (settings: AdvisorSettings) => {
125
127
  setAdvisorGitContextRef(settings.gitContext ?? "summary");
126
128
  setAdvisorGitContextMaxCharsRef(settings.gitContextMaxChars ?? 20_000);
127
129
  setAdvisorToolPoliciesRef(settings.toolPolicies ?? {});
130
+ setAdvisorAgentsMdContextRef(settings.agentsMdContext ?? true);
128
131
  setAdvisorTrackedFileContentRef(settings.trackedFileContent ?? false);
129
132
  setAdvisorUntrackedContentRef(settings.untrackedContent ?? false);
130
133
  setAdvisorOutcomeLoggingRef(settings.outcomeLogging ?? false);
@@ -140,11 +143,17 @@ export const saveAdvisorSettings = (
140
143
  ctx: ExtensionContext,
141
144
  settings: AdvisorSettings
142
145
  ) => {
143
- applyAdvisorSettings(settings);
144
- const persisted = getPersistedModelRefs();
145
- saveConfig(ctx, {
146
- persistAdvisor: Boolean(persisted.advisor),
147
- persistExecutor: Boolean(persisted.executor),
148
- });
149
- saveGlobalOutcomeLogging(settings.outcomeLogging ?? false);
146
+ const previous = getAdvisorSettings();
147
+ try {
148
+ applyAdvisorSettings(settings);
149
+ const persisted = getPersistedModelRefs();
150
+ saveConfig(ctx, {
151
+ persistAdvisor: Boolean(persisted.advisor),
152
+ persistExecutor: Boolean(persisted.executor),
153
+ });
154
+ saveGlobalOutcomeLogging(settings.outcomeLogging ?? false);
155
+ } catch (error) {
156
+ applyAdvisorSettings(previous);
157
+ throw error;
158
+ }
150
159
  };
@@ -19,10 +19,11 @@ export type ManualConsult = (
19
19
  gitContext?: GitContextLevel
20
20
  ) => Promise<{
21
21
  adviceId?: string;
22
- markdown: string;
23
- thinkingText: string;
22
+ agentRulesBytes?: number;
24
23
  draftBytes?: number;
24
+ markdown: string;
25
25
  preferenceBytes?: number;
26
+ thinkingText: string;
26
27
  usage?: unknown;
27
28
  }>;
28
29
 
@@ -16,7 +16,9 @@ export const parseArgs = (args: string): string | undefined => {
16
16
  let nextAdvisor = advisorRef;
17
17
  let nextContextMaxChars = contextMaxCharsRef;
18
18
  for (const token of args.trim().split(ARGUMENT_WHITESPACE).filter(Boolean)) {
19
- const [key, value] = token.split("=");
19
+ const separator = token.indexOf("=");
20
+ const key = separator === -1 ? token : token.slice(0, separator);
21
+ const value = separator === -1 ? undefined : token.slice(separator + 1);
20
22
  if (key === "executor" && value) {
21
23
  nextExecutor = value;
22
24
  }
@@ -1,4 +1,5 @@
1
1
  import {
2
+ setAdvisorAgentsMdContextRef,
2
3
  setAdvisorAutoLoopGateRef,
3
4
  setAdvisorBlockOnBlockedRef,
4
5
  setAdvisorCollapseResponsesRef,
@@ -68,6 +69,7 @@ import {
68
69
  export const resetDefaults = () => {
69
70
  setExecutorRef("");
70
71
  setAdvisorRef("");
72
+ setAdvisorAgentsMdContextRef(true);
71
73
  setPersistedModelRefs(undefined, undefined);
72
74
  setExecutorEffortRef(undefined);
73
75
  setAdvisorEffortRef(undefined);
@@ -139,6 +141,11 @@ export const applyConfig = (config: AdvisorConfig) => {
139
141
  applyNonEmptyStringConfig(config.advisor, setAdvisorRef);
140
142
  applyNonEmptyStringConfig(config.executorEffort, setExecutorEffortRef);
141
143
  applyNonEmptyStringConfig(config.advisorEffort, setAdvisorEffortRef);
144
+ applyOptionalConfig(
145
+ config,
146
+ "advisorAgentsMdContext",
147
+ setAdvisorAgentsMdContextRef
148
+ );
142
149
  applyOptionalConfig(config, "contextMaxChars", setContextMaxCharsRef);
143
150
  applyOptionalConfig(config, "advisorPlanGate", setAdvisorPlanGateRef);
144
151
  applyOptionalConfig(config, "advisorFailureGate", setAdvisorFailureGateRef);
@@ -2,6 +2,7 @@ import { isString } from "../content-utils.ts";
2
2
  import type { JsonValue } from "../content-utils.ts";
3
3
  import { GIT_CONTEXT_LEVELS, isValidGitContextLevel } from "../git.ts";
4
4
  import {
5
+ advisorAgentsMdContextRef,
5
6
  advisorAutoLoopGateRef,
6
7
  advisorBlockOnBlockedRef,
7
8
  advisorCollapseResponsesRef,
@@ -171,6 +172,12 @@ export const CONFIG_SCHEMA = {
171
172
  persisted: true,
172
173
  type: "string",
173
174
  },
175
+ advisorAgentsMdContext: {
176
+ accepted: "true or false",
177
+ current: () => advisorAgentsMdContextRef,
178
+ persisted: true,
179
+ type: "boolean",
180
+ },
174
181
  advisorAutoLoopGate: {
175
182
  accepted: "true or false",
176
183
  current: () => advisorAutoLoopGateRef,
@@ -25,6 +25,7 @@ import type {
25
25
  // An empty ref means no model has been selected yet.
26
26
  export let executorRef = "";
27
27
  export let advisorRef = "";
28
+ export let advisorAgentsMdContextRef = true;
28
29
  let persistedExecutorRef: string | undefined;
29
30
  let persistedAdvisorRef: string | undefined;
30
31
  export let executorEffortRef: string | undefined;
@@ -80,6 +81,9 @@ export const setExecutorRef = (ref: string) => {
80
81
  export const setAdvisorRef = (ref: string) => {
81
82
  advisorRef = ref;
82
83
  };
84
+ export const setAdvisorAgentsMdContextRef = (enabled: boolean) => {
85
+ advisorAgentsMdContextRef = enabled;
86
+ };
83
87
  /** Returns model refs explicitly persisted in the global Advisor config. */
84
88
  export const getPersistedModelRefs = () => ({
85
89
  advisor: persistedAdvisorRef,
@@ -234,6 +238,7 @@ export const setShowUsageFooterRef = (enabled: boolean) => {
234
238
  * imported mutable bindings, which can be snapshotted by extension loaders.
235
239
  */
236
240
  export const getAdvisorSettings = () => ({
241
+ agentsMdContext: advisorAgentsMdContextRef,
237
242
  alwaysOn: alwaysOnRef,
238
243
  autoLoopGate: advisorAutoLoopGateRef,
239
244
  blockOnBlocked: advisorBlockOnBlockedRef,
@@ -52,6 +52,27 @@ export const readExistingConfig = (path: string): RecordValue => {
52
52
  }
53
53
  };
54
54
 
55
+ const readConfigForSave = (path: string): RecordValue => {
56
+ if (!existsSync(path)) {
57
+ return {};
58
+ }
59
+ let parsed: unknown;
60
+ try {
61
+ parsed = JSON.parse(readFileSync(path, "utf-8"));
62
+ } catch (error) {
63
+ throw new Error(
64
+ `Cannot save Advisor configuration at ${path}: existing JSON is malformed; fix advisor.json before saving.`,
65
+ { cause: error }
66
+ );
67
+ }
68
+ if (!isRecord(parsed)) {
69
+ throw new Error(
70
+ `Cannot save Advisor configuration at ${path}: configuration must be a JSON object.`
71
+ );
72
+ }
73
+ return parsed;
74
+ };
75
+
55
76
  const shouldPersistConfigKey = (
56
77
  key: SavedConfigKey,
57
78
  persistAdvisor: boolean,
@@ -197,7 +218,6 @@ export const saveConfig = (
197
218
  const path = join(getAgentDir(), "advisor.json");
198
219
  const persistAdvisor = options.persistAdvisor ?? true;
199
220
  const persistExecutor = options.persistExecutor ?? true;
200
- const existing = readExistingConfig(path);
201
221
  const current = currentConfigState();
202
222
  const baseline = loadedConfigPath === path ? loadedConfigState : undefined;
203
223
  const changedKeys = baseline
@@ -208,6 +228,7 @@ export const saveConfig = (
208
228
  if (changedKeys.length === 0) {
209
229
  return path;
210
230
  }
231
+ const existing = readConfigForSave(path);
211
232
  const data = applyChangedConfigValues(
212
233
  existing,
213
234
  current,
@@ -240,7 +261,7 @@ export const saveConfig = (
240
261
  /** Outcome logging is deliberately written only to the global Pi configuration. */
241
262
  export const saveGlobalOutcomeLogging = (enabled: boolean) => {
242
263
  const path = join(getAgentDir(), "advisor.json");
243
- const existing = readExistingConfig(path);
264
+ const existing = readConfigForSave(path);
244
265
  writeFileSync(
245
266
  path,
246
267
  `${JSON.stringify({ ...existing, advisorOutcomeLogging: enabled }, null, 2)}\n`
@@ -60,6 +60,7 @@ export const GATE_FAILURE_MODES: GateFailureMode[] = [
60
60
 
61
61
  export interface AdvisorConfig {
62
62
  advisor?: string;
63
+ advisorAgentsMdContext?: boolean;
63
64
  advisorAutoLoopGate?: boolean;
64
65
  advisorBlockOnBlocked?: boolean;
65
66
  advisorCollapseResponses?: boolean;
package/src/config.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  // biome-ignore lint/performance/noBarrelFile: this facade intentionally preserves the public config module contract.
2
2
  export { parseArgs } from "./config/args.ts";
3
3
  export {
4
+ advisorAgentsMdContextRef,
4
5
  advisorAutoLoopGateRef,
5
6
  advisorBlockOnBlockedRef,
6
7
  advisorCollapseResponsesRef,
@@ -34,6 +35,7 @@ export {
34
35
  getAdvisorSettings,
35
36
  getPersistedModelRefs,
36
37
  isSimpleMode,
38
+ setAdvisorAgentsMdContextRef,
37
39
  setAdvisorAutoLoopGateRef,
38
40
  setAdvisorBlockOnBlockedRef,
39
41
  setAdvisorCollapseResponsesRef,
@@ -72,9 +72,13 @@ export const readImageFiles = async (
72
72
  remainingBytes: number,
73
73
  remainingCount: number
74
74
  ): Promise<{ images: ImageAttachment[]; omitted: number }> => {
75
- const names = Array.isArray(requested)
76
- ? requested.filter((path) => isString(path) && imageFileCandidate(path))
77
- : [];
75
+ const names = [
76
+ ...new Set(
77
+ Array.isArray(requested)
78
+ ? requested.filter((path) => isString(path) && imageFileCandidate(path))
79
+ : []
80
+ ),
81
+ ];
78
82
  if (!enabled || !names.length) {
79
83
  return { images: [], omitted: names.length };
80
84
  }