pi-advisor-flow 0.6.0 → 0.8.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.
Files changed (93) hide show
  1. package/CHANGELOG.md +34 -17
  2. package/README.md +10 -8
  3. package/dist/index.js +2852 -868
  4. package/extensions/index.ts +2 -1
  5. package/package.json +55 -40
  6. package/src/attachments.ts +11 -10
  7. package/src/commands/activation-preparation.ts +1 -0
  8. package/src/commands/activation.ts +2 -0
  9. package/src/commands/lifecycle.ts +1 -0
  10. package/src/commands/manual-command.ts +7 -1
  11. package/src/commands/manual-consultation.ts +103 -79
  12. package/src/commands/manual-progress.ts +7 -5
  13. package/src/commands/model-commands.ts +1 -1
  14. package/src/commands/model-options.ts +14 -3
  15. package/src/commands/model-picker.ts +1 -0
  16. package/src/commands/registration.ts +1 -0
  17. package/src/commands/renderers.ts +67 -55
  18. package/src/commands/runtime.ts +16 -14
  19. package/src/commands/settings-commands.ts +10 -4
  20. package/src/commands/settings-persistence.ts +64 -2
  21. package/src/commands/types.ts +2 -0
  22. package/src/commands.ts +0 -1
  23. package/src/config/args.ts +1 -1
  24. package/src/config/defaults.ts +83 -1
  25. package/src/config/schema.ts +152 -15
  26. package/src/config/state.ts +80 -2
  27. package/src/config/storage.ts +38 -22
  28. package/src/config/types.ts +45 -3
  29. package/src/config/validation.ts +33 -21
  30. package/src/config.ts +3 -0
  31. package/src/content-utils.ts +24 -4
  32. package/src/conversation.ts +16 -15
  33. package/src/git.ts +10 -3
  34. package/src/herdr-block.ts +83 -0
  35. package/src/herdr-shared.ts +108 -0
  36. package/src/herdr.ts +30 -192
  37. package/src/jev/client.ts +338 -0
  38. package/src/jev/failure.ts +19 -0
  39. package/src/jev/key-store.ts +267 -0
  40. package/src/jev/ledger.ts +228 -0
  41. package/src/jev/questions.ts +122 -0
  42. package/src/jev/state.ts +50 -0
  43. package/src/jev/transport.ts +63 -0
  44. package/src/model-stream.ts +35 -37
  45. package/src/outcomes.ts +20 -12
  46. package/src/pi-settings.ts +4 -9
  47. package/src/preferences.ts +4 -2
  48. package/src/redaction.ts +10 -12
  49. package/src/scout-context.ts +59 -60
  50. package/src/scout-curation.ts +4 -13
  51. package/src/scout-groups.ts +178 -175
  52. package/src/scout-protocol.ts +74 -70
  53. package/src/scout-reconstruct.ts +1 -1
  54. package/src/scout.ts +115 -73
  55. package/src/session-state.ts +148 -39
  56. package/src/tool-result-cap.ts +18 -17
  57. package/src/tools/consult-context.ts +7 -15
  58. package/src/tools/consultation.ts +14 -4
  59. package/src/tools/gate-policy.ts +1 -0
  60. package/src/tools/gate-protocol.ts +36 -23
  61. package/src/tools/jev-filter.ts +204 -0
  62. package/src/tools/jev-turn-gate.ts +203 -0
  63. package/src/tools/loop-gate.ts +32 -20
  64. package/src/tools/model-access.ts +46 -0
  65. package/src/tools/outage-notifier.ts +30 -0
  66. package/src/tools/prompts.ts +20 -25
  67. package/src/tools/register-ask-advisor.ts +73 -17
  68. package/src/tools/register-lifecycle.ts +22 -0
  69. package/src/tools/register-outcome.ts +2 -0
  70. package/src/tools/register-renderers.ts +149 -73
  71. package/src/tools/registration.ts +15 -0
  72. package/src/tools/render-advisor-result.ts +77 -35
  73. package/src/tools/render-common.ts +16 -19
  74. package/src/tools/scout-status.ts +42 -27
  75. package/src/tools/types.ts +11 -0
  76. package/src/tools.ts +0 -1
  77. package/src/ui/jev-setup-submenu.ts +350 -0
  78. package/src/ui/manual-dialog-render.ts +1 -0
  79. package/src/ui/manual-dialog.ts +39 -43
  80. package/src/ui/masked-input.ts +62 -0
  81. package/src/ui/model-multi-selector.ts +9 -0
  82. package/src/ui/model-selector-adapter.ts +31 -0
  83. package/src/ui/model-selector.ts +14 -161
  84. package/src/ui/searchable-model-list.ts +230 -0
  85. package/src/ui/settings-formatting.ts +24 -14
  86. package/src/ui/settings-items.ts +200 -2
  87. package/src/ui/settings-list-adapter.ts +15 -12
  88. package/src/ui/settings-mutations.ts +169 -47
  89. package/src/ui/settings-selector.ts +18 -9
  90. package/src/ui/text-setting-submenu.ts +3 -6
  91. package/src/ui/types.ts +25 -0
  92. package/src/ui.ts +0 -1
  93. package/src/usage.ts +29 -20
@@ -0,0 +1,63 @@
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+
3
+ import { advisorJevTransportRef } from "../config/state.ts";
4
+ import { resolveTypeSafeKey } from "./key-store.ts";
5
+ import type { JevKeySource } from "./key-store.ts";
6
+
7
+ export type JevTransportKind = "typesafe" | "openrouter";
8
+
9
+ export interface JevCredentials {
10
+ apiKey: string;
11
+ /** Where the TypeSafe key came from; present on the typesafe transport. */
12
+ source?: JevKeySource;
13
+ transport: JevTransportKind;
14
+ }
15
+
16
+ export interface JevTransportDeps {
17
+ /** Reads a pi-stored provider login; injectable for tests. */
18
+ getProviderKey?: (provider: string) => Promise<string | undefined>;
19
+ /** Resolves the TypeSafe key chain; injectable for tests. */
20
+ resolveTypesafe?: () => ReturnType<typeof resolveTypeSafeKey>;
21
+ }
22
+
23
+ const OPENROUTER_PROVIDER = "openrouter";
24
+
25
+ const openRouterKey = async (
26
+ ctx: ExtensionContext | undefined,
27
+ deps: JevTransportDeps
28
+ ): Promise<string | undefined> => {
29
+ const key = deps.getProviderKey
30
+ ? await deps.getProviderKey(OPENROUTER_PROVIDER)
31
+ : await ctx?.modelRegistry?.getApiKeyForProvider(OPENROUTER_PROVIDER);
32
+ return key?.trim() || undefined;
33
+ };
34
+
35
+ /** Resolves how Jev calls authenticate: a dedicated TypeSafe key first, then
36
+ * the existing pi OpenRouter login (reuse per advisorJevTransport). */
37
+ export const resolveJevTransport = async (
38
+ ctx?: ExtensionContext,
39
+ deps: JevTransportDeps = {}
40
+ ): Promise<JevCredentials | undefined> => {
41
+ const preference = advisorJevTransportRef;
42
+ if (preference !== "openrouter") {
43
+ const resolveTypesafe = deps.resolveTypesafe ?? resolveTypeSafeKey;
44
+ const resolution = await resolveTypesafe();
45
+ if (resolution.key) {
46
+ const credentials: JevCredentials = {
47
+ apiKey: resolution.key,
48
+ transport: "typesafe",
49
+ };
50
+ if (resolution.source) {
51
+ credentials.source = resolution.source;
52
+ }
53
+ return credentials;
54
+ }
55
+ }
56
+ if (preference === "typesafe") {
57
+ return undefined;
58
+ }
59
+ const openrouter = await openRouterKey(ctx, deps);
60
+ return openrouter
61
+ ? { apiKey: openrouter, transport: "openrouter" }
62
+ : undefined;
63
+ };
@@ -1,11 +1,12 @@
1
- import {
2
- type Api,
3
- type AssistantMessage,
4
- type Message,
5
- type Model,
6
- stream,
1
+ import { stream } from "@earendil-works/pi-ai/compat";
2
+ import type {
3
+ Api,
4
+ AssistantMessage,
5
+ Message,
6
+ Model,
7
7
  } from "@earendil-works/pi-ai/compat";
8
8
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
9
+
9
10
  import { splitRef } from "./config/state.ts";
10
11
 
11
12
  export interface ResolvedConfiguredModel {
@@ -25,13 +26,14 @@ export const resolveConfiguredModel = async (
25
26
  throw new Error(`${label} model not configured`);
26
27
  }
27
28
  const [provider, modelId] = splitRef(ref);
28
- const model = ctx.modelRegistry.find(provider, modelId);
29
+ const lookup: [string, string] = [provider, modelId];
30
+ const model = ctx.modelRegistry.find(...lookup);
29
31
  if (!model) {
30
32
  throw new Error(`${label} model not found: ${ref}`);
31
33
  }
32
34
  const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
33
35
  if (!auth.ok) {
34
- throw new Error((auth as { error: string }).error);
36
+ throw new Error(auth.error);
35
37
  }
36
38
  if (!auth.apiKey) {
37
39
  throw new Error(`No API key for ${ref}`);
@@ -61,6 +63,12 @@ export interface CollectedTextStream {
61
63
 
62
64
  export const ADVISOR_STREAM_UPDATE_INTERVAL_MS = 90;
63
65
 
66
+ const defaultScheduler: CoalescedUpdateScheduler = {
67
+ clearTimeout,
68
+ now: Date.now,
69
+ setTimeout: (callback, delay) => setTimeout(callback, delay),
70
+ };
71
+
64
72
  interface CoalescedUpdateResult {
65
73
  error?: unknown;
66
74
  failed: boolean;
@@ -85,19 +93,14 @@ export interface CoalescedUpdateScheduler {
85
93
  export const createCoalescedUpdate = <T>(
86
94
  publish: (value: T) => void,
87
95
  intervalMs = ADVISOR_STREAM_UPDATE_INTERVAL_MS,
88
- scheduler: CoalescedUpdateScheduler = {
89
- clearTimeout,
90
- now: Date.now,
91
- setTimeout: (callback, delay) => setTimeout(callback, delay),
92
- }
96
+ scheduler: CoalescedUpdateScheduler | undefined = defaultScheduler
93
97
  ): CoalescedUpdate<T> => {
94
98
  if (!Number.isFinite(intervalMs) || intervalMs <= 0) {
95
99
  throw new Error("Coalesced update interval must be positive and finite.");
96
100
  }
97
101
 
98
102
  let closed = false;
99
- let hasPending = false;
100
- let pending: T | undefined;
103
+ let pending: { value: T } | undefined;
101
104
  let lastPublishedAt: number | undefined;
102
105
  let timer: ReturnType<typeof setTimeout> | undefined;
103
106
  let publishError: unknown;
@@ -112,12 +115,11 @@ export const createCoalescedUpdate = <T>(
112
115
 
113
116
  const publishPending = () => {
114
117
  timer = undefined;
115
- if (!hasPending) {
118
+ if (!pending) {
116
119
  return;
117
120
  }
118
- const value = pending as T;
121
+ const { value } = pending;
119
122
  pending = undefined;
120
- hasPending = false;
121
123
  lastPublishedAt = scheduler.now();
122
124
  try {
123
125
  publish(value);
@@ -150,7 +152,6 @@ export const createCoalescedUpdate = <T>(
150
152
  closed = true;
151
153
  clearTimer();
152
154
  pending = undefined;
153
- hasPending = false;
154
155
  },
155
156
  flush: () => {
156
157
  if (!closed) {
@@ -167,8 +168,7 @@ export const createCoalescedUpdate = <T>(
167
168
  if (publishFailed) {
168
169
  throw publishError;
169
170
  }
170
- pending = value;
171
- hasPending = true;
171
+ pending = { value };
172
172
  if (timer === undefined) {
173
173
  schedule();
174
174
  }
@@ -186,22 +186,22 @@ export const collectTextStream = async (
186
186
  ): Promise<CollectedTextStream> => {
187
187
  let thinking = "";
188
188
  let text = "";
189
+ const streamOptions: Parameters<typeof streamModel>[2] = {
190
+ apiKey: resolved.apiKey,
191
+ env: resolved.env,
192
+ headers: resolved.headers,
193
+ // SAFETY: stream() models the provider-facing reasoning field as never; options.reasoning is the Pi-facing effort string.
194
+ reasoning: options.reasoning as never,
195
+ signal: options.signal,
196
+ };
197
+ if (options.reasoning !== undefined) {
198
+ // SAFETY: reasoningEffort takes the same effort string; kept absent when reasoning is unset.
199
+ streamOptions.reasoningEffort = options.reasoning as never;
200
+ }
189
201
  const eventStream = streamModel(
190
202
  resolved.model,
191
203
  { messages: options.messages, systemPrompt: options.systemPrompt },
192
- {
193
- apiKey: resolved.apiKey,
194
- env: resolved.env,
195
- headers: resolved.headers,
196
- // `stream()` uses the provider-facing name while the extension's public
197
- // option keeps the Pi-facing `reasoning` name. Preserve both so the
198
- // configured effort reaches providers that serialize reasoning_effort.
199
- reasoning: options.reasoning as never,
200
- ...(options.reasoning === undefined
201
- ? {}
202
- : { reasoningEffort: options.reasoning as never }),
203
- signal: options.signal,
204
- }
204
+ streamOptions
205
205
  );
206
206
 
207
207
  for await (const event of eventStream) {
@@ -242,8 +242,6 @@ export const collectTextStream = async (
242
242
  return {
243
243
  text: finalText,
244
244
  thinking,
245
- usage: (
246
- lastAssistant as (AssistantMessage & { usage?: unknown }) | undefined
247
- )?.usage,
245
+ usage: lastAssistant?.usage,
248
246
  };
249
247
  };
package/src/outcomes.ts CHANGED
@@ -11,11 +11,17 @@ import {
11
11
  writeFile,
12
12
  } from "node:fs/promises";
13
13
  import { join } from "node:path";
14
+ import { setTimeout as sleep } from "node:timers/promises";
15
+
14
16
  import { getAgentDir } from "@earendil-works/pi-coding-agent";
15
17
 
16
18
  export type OutcomeAdoption = "followed" | "not-followed" | "unknown";
17
19
  export type OutcomeValidation = "passed" | "failed" | "not-run" | "unknown";
18
- type OutcomeTrigger = "manual" | "executor-requested" | "repeated-tool-call";
20
+ type OutcomeTrigger =
21
+ | "manual"
22
+ | "executor-requested"
23
+ | "turn-gate"
24
+ | "repeated-tool-call";
19
25
  export const ADOPTIONS: OutcomeAdoption[] = [
20
26
  "followed",
21
27
  "not-followed",
@@ -40,12 +46,15 @@ const statePath = () => join(getAgentDir(), "advisor-outcomes-salt");
40
46
  export const outcomeLogPath = () =>
41
47
  join(getAgentDir(), "advisor-outcomes.jsonl");
42
48
 
49
+ const isErrnoException = (error: unknown): error is NodeJS.ErrnoException =>
50
+ error instanceof Error && "code" in error;
51
+
43
52
  const salt = async () => {
44
53
  const path = statePath();
45
54
  await mkdir(getAgentDir(), { mode: 0o700, recursive: true });
46
55
  for (let attempt = 0; attempt < 20; attempt += 1) {
47
56
  try {
48
- // biome-ignore lint/performance/noAwaitInLoops: contenders retry sequentially until one salt is atomically published.
57
+ // Contenders retry sequentially until one salt is atomically published.
49
58
  const existing = await readFile(path);
50
59
  if (existing.length === 32) {
51
60
  return existing;
@@ -53,7 +62,7 @@ const salt = async () => {
53
62
  // Recover a salt file left incomplete by an interrupted older writer.
54
63
  await unlink(path);
55
64
  } catch (error) {
56
- if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
65
+ if (!isErrnoException(error) || error.code !== "ENOENT") {
57
66
  throw error;
58
67
  }
59
68
  }
@@ -64,7 +73,7 @@ const salt = async () => {
64
73
  await link(temporary, path);
65
74
  return value;
66
75
  } catch (error) {
67
- if ((error as NodeJS.ErrnoException).code !== "EEXIST") {
76
+ if (!isErrnoException(error) || error.code !== "EEXIST") {
68
77
  throw error;
69
78
  }
70
79
  } finally {
@@ -82,7 +91,7 @@ const withOutcomeLock = async <T>(run: () => Promise<T>): Promise<T> => {
82
91
  const lockPath = `${outcomeLogPath()}.lock`;
83
92
  for (let attempt = 0; attempt < 200; attempt += 1) {
84
93
  try {
85
- // biome-ignore lint/performance/noAwaitInLoops: lock acquisition must retry sequentially across processes.
94
+ // Lock acquisition must retry sequentially across processes.
86
95
  const lock = await open(lockPath, "wx", 0o600);
87
96
  const identity = await lock.stat();
88
97
  try {
@@ -95,7 +104,7 @@ const withOutcomeLock = async <T>(run: () => Promise<T>): Promise<T> => {
95
104
  }
96
105
  }
97
106
  } catch (error) {
98
- if ((error as NodeJS.ErrnoException).code !== "EEXIST") {
107
+ if (!isErrnoException(error) || error.code !== "EEXIST") {
99
108
  throw error;
100
109
  }
101
110
  const observed = await stat(lockPath).catch(() => undefined);
@@ -106,7 +115,7 @@ const withOutcomeLock = async <T>(run: () => Promise<T>): Promise<T> => {
106
115
  }
107
116
  continue;
108
117
  }
109
- await new Promise((resolve) => setTimeout(resolve, 5));
118
+ await sleep(5);
110
119
  }
111
120
  }
112
121
  throw new Error("Timed out waiting to append an Advisor outcome.");
@@ -141,11 +150,10 @@ export const appendOutcome = async (
141
150
  }
142
151
  throw error;
143
152
  });
144
- if (currentBytes + Buffer.byteLength(line) > MAX_LOG_BYTES) {
145
- await writeFile(path, line, { encoding: "utf8", mode: 0o600 });
146
- } else {
147
- await appendFile(path, line, { encoding: "utf8", mode: 0o600 });
148
- }
153
+ const overflow = currentBytes + Buffer.byteLength(line) > MAX_LOG_BYTES;
154
+ await (overflow
155
+ ? writeFile(path, line, { encoding: "utf-8", mode: 0o600 })
156
+ : appendFile(path, line, { encoding: "utf-8", mode: 0o600 }));
149
157
  await chmod(path, 0o600);
150
158
  return next;
151
159
  });
@@ -1,10 +1,9 @@
1
1
  import { existsSync, readFileSync, statSync } from "node:fs";
2
2
  import { join } from "node:path";
3
+
3
4
  import { getAgentDir } from "@earendil-works/pi-coding-agent";
4
5
 
5
- interface PiGlobalSettings {
6
- hideThinkingBlock?: boolean;
7
- }
6
+ import { isRecord } from "./content-utils.ts";
8
7
 
9
8
  interface CachedSetting {
10
9
  hidden: boolean;
@@ -27,12 +26,8 @@ const settingsIdentity = (path: string): string => {
27
26
 
28
27
  const readHideThinking = (path: string): boolean => {
29
28
  try {
30
- const parsed: unknown = JSON.parse(readFileSync(path, "utf8"));
31
- return (
32
- typeof parsed === "object" &&
33
- parsed !== null &&
34
- (parsed as PiGlobalSettings).hideThinkingBlock === true
35
- );
29
+ const parsed: unknown = JSON.parse(readFileSync(path, "utf-8"));
30
+ return isRecord(parsed) && parsed.hideThinkingBlock === true;
36
31
  } catch {
37
32
  return false;
38
33
  }
@@ -1,6 +1,8 @@
1
1
  import { lstat, open, realpath } from "node:fs/promises";
2
2
  import { join, relative } from "node:path";
3
+
3
4
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
5
+
4
6
  import { redactAndCapText } from "./redaction.ts";
5
7
 
6
8
  const PREFERENCES_MAX_BYTES = 8 * 1024;
@@ -40,9 +42,9 @@ export const readProjectPreferences = async (
40
42
  try {
41
43
  const buffer = Buffer.alloc(maxBytes + 1);
42
44
  const { bytesRead } = await file.read(buffer, 0, buffer.length, 0);
43
- const source = buffer.subarray(0, bytesRead).toString("utf8");
45
+ const source = buffer.subarray(0, bytesRead).toString("utf-8");
44
46
  const capped = redactAndCapText(source, maxBytes, redact);
45
- return { bytes: Buffer.byteLength(capped, "utf8"), text: capped };
47
+ return { bytes: Buffer.byteLength(capped, "utf-8"), text: capped };
46
48
  } finally {
47
49
  await file.close();
48
50
  }
package/src/redaction.ts CHANGED
@@ -1,15 +1,15 @@
1
- import { capUtf8Bytes } from "./content-utils.ts";
1
+ import { capUtf8Bytes, isString } from "./content-utils.ts";
2
2
 
3
3
  const REDACTION_MARKER = "[REDACTED SECRET]";
4
- const PEM_BEGIN_PATTERN = /-----BEGIN(?: [A-Z0-9]+)? PRIVATE KEY-----/gi;
5
- const PEM_END_PATTERN = /-----END(?: [A-Z0-9]+)? PRIVATE KEY-----/i;
4
+ const PEM_BEGIN_PATTERN = /-----BEGIN(?: [A-Z0-9]+)? PRIVATE KEY-----/giu;
5
+ const PEM_END_PATTERN = /-----END(?: [A-Z0-9]+)? PRIVATE KEY-----/iu;
6
6
  const SECRET_PATTERNS = [
7
- /-----BEGIN(?: [A-Z0-9]+)? PRIVATE KEY-----[\s\S]*?-----END(?: [A-Z0-9]+)? PRIVATE KEY-----/gi,
8
- /\bBearer\s+[A-Za-z0-9._~+/=-]{8,}/gi,
9
- /\b(?:api[_-]?key|token|secret|password|passwd)\s*[:=]\s*(?:"[^"]*"|'[^']*'|[^\s"'&,;)}\]]+)/gi,
10
- /([a-z][a-z0-9+.-]*:\/\/)[^\s/@:]+:[^\s/@]+@/gi,
11
- /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/g,
12
- /\b(?:aws_secret_access_key|aws_session_token)\s*[:=]\s*[^\s"'&,;)}\]]+/gi,
7
+ /-----BEGIN(?: [A-Z0-9]+)? PRIVATE KEY-----[\s\S]*?-----END(?: [A-Z0-9]+)? PRIVATE KEY-----/giu,
8
+ /\bBearer\s+[A-Za-z0-9._~+/=-]{8,}/giu,
9
+ /\b(?:api[_-]?key|token|secret|password|passwd)\s*[:=]\s*(?:"[^"]*"|'[^']*'|[^\s"'&,;)}\]]+)/giu,
10
+ /(?<scheme>[a-z][a-z0-9+.-]*:\/\/)[^\s/@:]+:[^\s/@]+@/giu,
11
+ /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/gu,
12
+ /\b(?:aws_secret_access_key|aws_session_token)\s*[:=]\s*[^\s"'&,;)}\]]+/giu,
13
13
  ] as const;
14
14
 
15
15
  const redactUnterminatedPem = (value: string): string => {
@@ -31,9 +31,7 @@ export const redactSecrets = (value: string): string => {
31
31
  let redacted = redactUnterminatedPem(value);
32
32
  for (const pattern of SECRET_PATTERNS) {
33
33
  redacted = redacted.replace(pattern, (_match, scheme) =>
34
- typeof scheme === "string"
35
- ? `${scheme}${REDACTION_MARKER}@`
36
- : REDACTION_MARKER
34
+ isString(scheme) ? `${scheme}${REDACTION_MARKER}@` : REDACTION_MARKER
37
35
  );
38
36
  }
39
37
  return redacted;
@@ -1,27 +1,26 @@
1
1
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+
2
3
  import {
3
4
  advisorRedactSecretsRef,
4
5
  advisorToolPoliciesRef,
5
6
  advisorToolResultMaxBytesRef,
6
7
  advisorToolResultMaxLinesRef,
7
8
  } from "./config/state.ts";
8
- import {
9
- buildGroups,
10
- type DisclosureCaps,
11
- type GroupPass,
12
- groupWireBytes,
13
- } from "./scout-groups.ts";
9
+ import { buildGroups, groupWireBytes } from "./scout-groups.ts";
10
+ import type { DisclosureCaps, GroupPass } from "./scout-groups.ts";
14
11
  import { indexToolCalls } from "./scout-protocol.ts";
15
12
  import {
16
- type BuildScoutManifestOptions,
17
13
  SCOUT_GROUP_MAX_BYTES,
18
14
  SCOUT_MANIFEST_MAX_BYTES,
19
15
  SCOUT_MANIFEST_MAX_GROUPS,
20
- type ScoutContextGroup,
21
- type ScoutManifestResult,
16
+ } from "./scout-types.ts";
17
+ import type {
18
+ BuildScoutManifestOptions,
19
+ ScoutContextGroup,
20
+ ScoutManifestResult,
22
21
  } from "./scout-types.ts";
23
22
 
24
- // biome-ignore lint/performance/noBarrelFile: re-export preserves scout-context's historical public import surface.
23
+ // Re-exports preserve scout-context's historical public import surface.
25
24
  export { reconstructScoutConversation } from "./scout-reconstruct.ts";
26
25
  export type {
27
26
  BuildScoutManifestOptions,
@@ -54,21 +53,44 @@ const resolveCaps = (
54
53
  });
55
54
 
56
55
  /** Builds disclosed, indivisible history groups from Pi's compaction-aware branch. */
57
- export const buildScoutManifest = (
58
- ctx: ExtensionContext,
59
- options: BuildScoutManifestOptions = {}
60
- ): ScoutManifestResult => {
61
- const entries = ctx.sessionManager.buildContextEntries();
62
- const caps = resolveCaps(options);
63
- const indexed = indexToolCalls(entries);
64
- if (!indexed.ok) {
65
- return indexed;
56
+ const contentChars = (items: ScoutContextGroup[]) =>
57
+ items.reduce((sum, group) => sum + group.content.length, 0) +
58
+ Math.max(0, items.length - 1) * 2;
59
+
60
+ const requiredOverflow = (
61
+ required: ScoutContextGroup[],
62
+ caps: {
63
+ maxConversationChars?: number;
64
+ maxGroupBytes: number;
65
+ maxGroups: number;
66
+ maxManifestBytes: number;
66
67
  }
67
- const built = buildGroups(entries, indexed.index, caps);
68
- if (!built.ok) {
69
- return built;
68
+ ): ScoutManifestResult | undefined => {
69
+ if (
70
+ required.some((group) => group.bytes > caps.maxGroupBytes) ||
71
+ required.length > caps.maxGroups ||
72
+ required.reduce((sum, group) => sum + groupWireBytes(group), 0) >
73
+ caps.maxManifestBytes
74
+ ) {
75
+ return {
76
+ message:
77
+ "Required Scout context exceeds the Scout manifest transport limit.",
78
+ ok: false,
79
+ reason: "required-group-overflow",
80
+ };
70
81
  }
71
- return fitToBudget(built, caps);
82
+ if (
83
+ caps.maxConversationChars !== undefined &&
84
+ contentChars(required) > caps.maxConversationChars
85
+ ) {
86
+ return {
87
+ message:
88
+ "Required Scout context exceeds the Advisor conversation budget.",
89
+ ok: false,
90
+ reason: "required-group-overflow",
91
+ };
92
+ }
93
+ return undefined;
72
94
  };
73
95
 
74
96
  const fits = (
@@ -86,10 +108,6 @@ const fits = (
86
108
  (caps.maxConversationChars === undefined ||
87
109
  contentChars(selected) <= caps.maxConversationChars);
88
110
 
89
- const contentChars = (items: ScoutContextGroup[]) =>
90
- items.reduce((sum, group) => sum + group.content.length, 0) +
91
- Math.max(0, items.length - 1) * 2;
92
-
93
111
  const fitToBudget = (
94
112
  built: GroupPass,
95
113
  caps: {
@@ -126,7 +144,7 @@ const fitToBudget = (
126
144
  );
127
145
  while (!fits(selected, caps)) {
128
146
  const optionalIndex = selected.findIndex((group) => !group.required);
129
- if (optionalIndex < 0) {
147
+ if (optionalIndex === -1) {
130
148
  return {
131
149
  message: "Required Scout context exceeds fixed manifest limits.",
132
150
  ok: false,
@@ -151,38 +169,19 @@ const fitToBudget = (
151
169
  };
152
170
  };
153
171
 
154
- const requiredOverflow = (
155
- required: ScoutContextGroup[],
156
- caps: {
157
- maxConversationChars?: number;
158
- maxGroupBytes: number;
159
- maxGroups: number;
160
- maxManifestBytes: number;
161
- }
162
- ): ScoutManifestResult | undefined => {
163
- if (
164
- required.some((group) => group.bytes > caps.maxGroupBytes) ||
165
- required.length > caps.maxGroups ||
166
- required.reduce((sum, group) => sum + groupWireBytes(group), 0) >
167
- caps.maxManifestBytes
168
- ) {
169
- return {
170
- message:
171
- "Required Scout context exceeds the Scout manifest transport limit.",
172
- ok: false,
173
- reason: "required-group-overflow",
174
- };
172
+ export const buildScoutManifest = (
173
+ ctx: ExtensionContext,
174
+ options: BuildScoutManifestOptions = {}
175
+ ): ScoutManifestResult => {
176
+ const entries = ctx.sessionManager.buildContextEntries();
177
+ const caps = resolveCaps(options);
178
+ const indexed = indexToolCalls(entries);
179
+ if (!indexed.ok) {
180
+ return indexed;
175
181
  }
176
- if (
177
- caps.maxConversationChars !== undefined &&
178
- contentChars(required) > caps.maxConversationChars
179
- ) {
180
- return {
181
- message:
182
- "Required Scout context exceeds the Advisor conversation budget.",
183
- ok: false,
184
- reason: "required-group-overflow",
185
- };
182
+ const built = buildGroups(entries, indexed.index, caps);
183
+ if (!built.ok) {
184
+ return built;
186
185
  }
187
- return undefined;
186
+ return fitToBudget(built, caps);
188
187
  };
@@ -1,15 +1,13 @@
1
1
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+
2
3
  import { advisorScoutEnabledRef, executorRef } from "./config/state.ts";
3
- import {
4
- runAdvisorScout,
5
- type ScoutLifecycleEvent,
6
- type ScoutOutcome,
7
- } from "./scout.ts";
8
4
  import {
9
5
  buildScoutManifest,
10
6
  reconstructScoutConversation,
11
7
  SCOUT_MANIFEST_MAX_BYTES,
12
8
  } from "./scout-context.ts";
9
+ import { runAdvisorScout } from "./scout.ts";
10
+ import type { ScoutLifecycleEvent, ScoutOutcome } from "./scout.ts";
13
11
 
14
12
  /** Runs Scout over the manifested branch and reconstructs the curated
15
13
  * conversation, falling back to the exact legacy conversation on any
@@ -55,14 +53,7 @@ export const curateAdvisorConversation = async (
55
53
  onScout?.({ outcome: scout, type: "fallback" });
56
54
  return { conversation: legacyConversation, scout };
57
55
  }
58
- const outcome = await runScout(
59
- ctx,
60
- built.manifest,
61
- signal,
62
- onScout,
63
- undefined,
64
- undefined
65
- );
56
+ const outcome = await runScout(ctx, built.manifest, signal, onScout);
66
57
  if (!outcome.ok && outcome.cancelled) {
67
58
  throw signal?.reason instanceof Error
68
59
  ? signal.reason