pi-advisor-flow 0.5.5 → 0.6.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 (76) hide show
  1. package/CHANGELOG.md +28 -1
  2. package/README.md +1 -1
  3. package/dist/index.js +5786 -0
  4. package/extensions/index.ts +9 -9
  5. package/package.json +16 -10
  6. package/src/{untracked.ts → attachments.ts} +2 -5
  7. package/src/commands/activation-preparation.ts +6 -6
  8. package/src/commands/activation.ts +7 -7
  9. package/src/commands/lifecycle.ts +5 -5
  10. package/src/commands/manual-command.ts +13 -9
  11. package/src/commands/manual-consultation.ts +8 -8
  12. package/src/commands/manual-progress.ts +3 -3
  13. package/src/commands/model-commands.ts +6 -6
  14. package/src/commands/model-options.ts +4 -4
  15. package/src/commands/model-picker.ts +2 -2
  16. package/src/commands/registration.ts +8 -8
  17. package/src/commands/renderers.ts +5 -5
  18. package/src/commands/runtime.ts +8 -8
  19. package/src/commands/settings-commands.ts +8 -8
  20. package/src/commands/settings-persistence.ts +4 -4
  21. package/src/commands/types.ts +5 -13
  22. package/src/commands.ts +2 -2
  23. package/src/config/args.ts +3 -3
  24. package/src/config/defaults.ts +3 -3
  25. package/src/config/schema.ts +322 -0
  26. package/src/config/state.ts +2 -2
  27. package/src/config/storage.ts +113 -103
  28. package/src/config/types.ts +1 -1
  29. package/src/config/validation.ts +61 -181
  30. package/src/config.ts +7 -7
  31. package/src/content-utils.ts +30 -0
  32. package/src/conversation.ts +13 -168
  33. package/src/git.ts +4 -18
  34. package/src/herdr.ts +6 -8
  35. package/src/model-stream.ts +5 -8
  36. package/src/outcomes.ts +2 -5
  37. package/src/pi-settings.ts +59 -0
  38. package/src/preferences.ts +2 -2
  39. package/src/redaction.ts +50 -0
  40. package/src/scout-context.ts +121 -485
  41. package/src/scout-curation.ts +84 -0
  42. package/src/scout-groups.ts +485 -0
  43. package/src/scout-protocol.ts +122 -0
  44. package/src/scout-reconstruct.ts +44 -0
  45. package/src/scout-types.ts +66 -0
  46. package/src/scout.ts +53 -63
  47. package/src/session-state.ts +119 -76
  48. package/src/tool-result-cap.ts +88 -0
  49. package/src/tools/consult-context.ts +155 -0
  50. package/src/tools/consultation.ts +71 -220
  51. package/src/tools/gate-policy.ts +5 -5
  52. package/src/tools/gate-protocol.ts +2 -2
  53. package/src/tools/loop-gate.ts +70 -61
  54. package/src/tools/prompts.ts +4 -7
  55. package/src/tools/register-ask-advisor.ts +9 -14
  56. package/src/tools/register-lifecycle.ts +7 -7
  57. package/src/tools/register-outcome.ts +5 -5
  58. package/src/tools/register-renderers.ts +5 -5
  59. package/src/tools/registration.ts +10 -10
  60. package/src/tools/render-advisor-result.ts +11 -8
  61. package/src/tools/render-common.ts +25 -13
  62. package/src/tools/scout-status.ts +5 -5
  63. package/src/tools/session.ts +1 -1
  64. package/src/tools/types.ts +7 -7
  65. package/src/tools.ts +9 -9
  66. package/src/ui/manual-dialog-render.ts +10 -3
  67. package/src/ui/manual-dialog.ts +4 -9
  68. package/src/ui/model-selector.ts +7 -6
  69. package/src/ui/settings-formatting.ts +2 -2
  70. package/src/ui/settings-items.ts +4 -4
  71. package/src/ui/settings-mutations.ts +2 -2
  72. package/src/ui/settings-selector.ts +5 -5
  73. package/src/ui/text-setting-submenu.ts +6 -1
  74. package/src/ui/types.ts +2 -2
  75. package/src/ui.ts +4 -4
  76. package/src/usage.ts +1 -6
@@ -1,72 +1,29 @@
1
- import { GIT_CONTEXT_LEVELS, isValidGitContextLevel } from "../git.js";
2
- import {
3
- ADVISOR_TOOL_POLICIES,
4
- type AdvisorConfig,
5
- GATE_FAILURE_MODES,
6
- MAX_CONTEXT_MAX_CHARS,
7
- } from "./types.js";
8
-
9
- const CONFIG_KEYS = new Set<keyof AdvisorConfig>([
10
- "advisor",
11
- "advisorAutoLoopGate",
12
- "advisorBlockOnBlocked",
13
- "advisorCollapseResponses",
14
- "advisorCompletionGate",
15
- "advisorCustomInvocation",
16
- "advisorEffort",
17
- "advisorFailureGate",
18
- "advisorGitContext",
19
- "advisorGitContextMaxChars",
20
- "advisorHerdrIntegration",
21
- "advisorLoopThreshold",
22
- "advisorMaxCallsPerSession",
23
- "advisorPlanGate",
24
- "advisorSessionSummary",
25
- "advisorScoutEnabled",
26
- "showUsageDetails",
27
- "showUsageFooter",
28
- "simpleMode",
29
- "alwaysOn",
30
- "advisorToolResultMaxBytes",
31
- "advisorToolResultMaxLines",
32
- "advisorTrackedFileContent",
33
- "advisorRedactSecrets",
34
- "advisorUntrackedContent",
35
- "advisorOutcomeLogging",
36
- "advisorToolPolicies",
37
- "contextMaxChars",
38
- "executor",
39
- "executorEffort",
40
- "gateFailureMode",
41
- ]);
42
-
43
- const BOOLEAN_CONFIG_KEYS = [
44
- "advisorPlanGate",
45
- "advisorFailureGate",
46
- "advisorCompletionGate",
47
- "advisorCollapseResponses",
48
- "advisorBlockOnBlocked",
49
- "advisorAutoLoopGate",
50
- "advisorSessionSummary",
51
- "advisorScoutEnabled",
52
- "showUsageDetails",
53
- "showUsageFooter",
54
- "simpleMode",
55
- "alwaysOn",
56
- "advisorHerdrIntegration",
57
- "advisorTrackedFileContent",
58
- "advisorRedactSecrets",
59
- "advisorUntrackedContent",
60
- "advisorOutcomeLogging",
61
- ] as const;
1
+ import type { ConfigKeySchema } from "./schema.ts";
2
+ import { CONFIG_SCHEMA, type ConfigKey, SCHEMA_BY_KEY } from "./schema.ts";
3
+ import type { AdvisorConfig } from "./types.ts";
4
+
5
+ // biome-ignore lint/performance/noBarrelFile: re-exports keep the isValid* validators on their historical module for the config facade.
6
+ export {
7
+ isValidAdvisorToolPolicies,
8
+ isValidContextMaxChars,
9
+ isValidGateFailureMode,
10
+ isValidLoopThreshold,
11
+ isValidMaxCallsPerSession,
12
+ isValidToolResultMaxBytes,
13
+ isValidToolResultMaxLines,
14
+ } from "./schema.ts";
15
+
16
+ const CONFIG_KEYS = new Set<ConfigKey>(
17
+ Object.keys(CONFIG_SCHEMA) as ConfigKey[]
18
+ );
19
+
20
+ const keysOfType = (type: ConfigKeySchema["type"]): readonly ConfigKey[] =>
21
+ (Object.keys(CONFIG_SCHEMA) as ConfigKey[]).filter(
22
+ (key) => SCHEMA_BY_KEY[key].type === type
23
+ );
62
24
 
63
- const STRING_CONFIG_KEYS = [
64
- "executor",
65
- "advisor",
66
- "executorEffort",
67
- "advisorEffort",
68
- "advisorCustomInvocation",
69
- ] as const;
25
+ const BOOLEAN_CONFIG_KEYS = keysOfType("boolean");
26
+ const STRING_CONFIG_KEYS = keysOfType("string");
70
27
 
71
28
  type ConfigRecord = Record<string, unknown>;
72
29
 
@@ -81,20 +38,12 @@ const invalidConfigValue = (
81
38
  };
82
39
 
83
40
  export const unknownConfigKeys = (config: ConfigRecord) =>
84
- Object.keys(config).filter(
85
- (key) => !CONFIG_KEYS.has(key as keyof AdvisorConfig)
86
- );
41
+ Object.keys(config).filter((key) => !CONFIG_KEYS.has(key as ConfigKey));
87
42
 
88
43
  const validateStringValues = (config: ConfigRecord, path: string) => {
89
44
  for (const key of STRING_CONFIG_KEYS) {
90
45
  if (config[key] !== undefined && typeof config[key] !== "string") {
91
- invalidConfigValue(
92
- path,
93
- key,
94
- key === "executor" || key === "advisor"
95
- ? "a provider/model string"
96
- : "a string"
97
- );
46
+ invalidConfigValue(path, key, SCHEMA_BY_KEY[key].accepted);
98
47
  }
99
48
  }
100
49
  };
@@ -102,91 +51,46 @@ const validateStringValues = (config: ConfigRecord, path: string) => {
102
51
  const validateBooleanValues = (config: ConfigRecord, path: string) => {
103
52
  for (const key of BOOLEAN_CONFIG_KEYS) {
104
53
  if (config[key] !== undefined && typeof config[key] !== "boolean") {
105
- invalidConfigValue(path, key, "true or false");
54
+ invalidConfigValue(path, key, SCHEMA_BY_KEY[key].accepted);
106
55
  }
107
56
  }
108
57
  };
109
58
 
110
- export const isValidAdvisorToolPolicies = (
111
- value: unknown
112
- ): value is Record<string, (typeof ADVISOR_TOOL_POLICIES)[number]> => {
113
- if (!value || typeof value !== "object" || Array.isArray(value)) {
114
- return false;
59
+ const validateNumericValues = (config: ConfigRecord, path: string) => {
60
+ for (const key of keysOfType("number")) {
61
+ const isValid = SCHEMA_BY_KEY[key].validate;
62
+ if (
63
+ config[key] !== undefined &&
64
+ isValid !== undefined &&
65
+ !isValid(config[key])
66
+ ) {
67
+ invalidConfigValue(path, key, SCHEMA_BY_KEY[key].accepted);
68
+ }
115
69
  }
116
- return Object.entries(value).every(
117
- ([toolName, policy]) =>
118
- toolName.trim().length > 0 &&
119
- typeof policy === "string" &&
120
- ADVISOR_TOOL_POLICIES.includes(
121
- policy as (typeof ADVISOR_TOOL_POLICIES)[number]
122
- )
123
- );
124
70
  };
125
71
 
126
- export const isValidContextMaxChars = (value: unknown): value is number =>
127
- typeof value === "number" &&
128
- Number.isSafeInteger(value) &&
129
- value >= 0 &&
130
- value <= MAX_CONTEXT_MAX_CHARS;
131
-
132
- export const isValidLoopThreshold = (value: unknown): value is number =>
133
- typeof value === "number" && Number.isSafeInteger(value) && value >= 2;
134
-
135
- export const isValidMaxCallsPerSession = (value: unknown): value is number =>
136
- typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
137
-
138
- export const isValidGateFailureMode = (
139
- value: unknown
140
- ): value is (typeof GATE_FAILURE_MODES)[number] =>
141
- typeof value === "string" &&
142
- GATE_FAILURE_MODES.includes(value as (typeof GATE_FAILURE_MODES)[number]);
143
-
144
- export const isValidToolResultMaxLines = (value: unknown): value is number =>
145
- typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
146
-
147
- export const isValidToolResultMaxBytes = (value: unknown): value is number =>
148
- typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
72
+ const validateEnumValues = (config: ConfigRecord, path: string) => {
73
+ for (const key of keysOfType("enum")) {
74
+ const isValid = SCHEMA_BY_KEY[key].validate;
75
+ if (
76
+ config[key] !== undefined &&
77
+ isValid !== undefined &&
78
+ !isValid(config[key])
79
+ ) {
80
+ invalidConfigValue(path, key, SCHEMA_BY_KEY[key].accepted);
81
+ }
82
+ }
83
+ };
149
84
 
150
- const validateNumericValues = (config: ConfigRecord, path: string) => {
151
- const numericRules: [
152
- keyof AdvisorConfig,
153
- (value: unknown) => boolean,
154
- string,
155
- ][] = [
156
- [
157
- "contextMaxChars",
158
- isValidContextMaxChars,
159
- `a safe integer from 0 through ${MAX_CONTEXT_MAX_CHARS}`,
160
- ],
161
- [
162
- "advisorLoopThreshold",
163
- isValidLoopThreshold,
164
- "a safe integer of at least 2",
165
- ],
166
- [
167
- "advisorMaxCallsPerSession",
168
- isValidMaxCallsPerSession,
169
- "a non-negative safe integer",
170
- ],
171
- [
172
- "advisorToolResultMaxLines",
173
- isValidToolResultMaxLines,
174
- "a non-negative safe integer",
175
- ],
176
- [
177
- "advisorToolResultMaxBytes",
178
- isValidToolResultMaxBytes,
179
- "a non-negative safe integer",
180
- ],
181
- [
182
- "advisorGitContextMaxChars",
183
- isValidToolResultMaxBytes,
184
- "a non-negative safe integer",
185
- ],
186
- ];
187
- for (const [key, isValid, description] of numericRules) {
188
- if (config[key] !== undefined && !isValid(config[key])) {
189
- invalidConfigValue(path, key, description);
85
+ const validateObjectValues = (config: ConfigRecord, path: string) => {
86
+ for (const key of keysOfType("object")) {
87
+ const isValid = SCHEMA_BY_KEY[key].validate;
88
+ if (
89
+ config[key] !== undefined &&
90
+ isValid !== undefined &&
91
+ !isValid(config[key])
92
+ ) {
93
+ invalidConfigValue(path, key, SCHEMA_BY_KEY[key].accepted);
190
94
  }
191
95
  }
192
96
  };
@@ -204,31 +108,7 @@ export const validateConfig = (
204
108
  validateStringValues(config, path);
205
109
  validateBooleanValues(config, path);
206
110
  validateNumericValues(config, path);
207
- if (
208
- config.advisorToolPolicies !== undefined &&
209
- !isValidAdvisorToolPolicies(config.advisorToolPolicies)
210
- ) {
211
- invalidConfigValue(
212
- path,
213
- "advisorToolPolicies",
214
- "a JSON object with non-empty tool names and full, summary, or exclude values"
215
- );
216
- }
217
- if (
218
- config.gateFailureMode !== undefined &&
219
- !isValidGateFailureMode(config.gateFailureMode)
220
- ) {
221
- invalidConfigValue(path, "gateFailureMode", GATE_FAILURE_MODES.join(", "));
222
- }
223
- if (
224
- config.advisorGitContext !== undefined &&
225
- !isValidGitContextLevel(config.advisorGitContext)
226
- ) {
227
- invalidConfigValue(
228
- path,
229
- "advisorGitContext",
230
- GIT_CONTEXT_LEVELS.join(", ")
231
- );
232
- }
111
+ validateObjectValues(config, path);
112
+ validateEnumValues(config, path);
233
113
  return true;
234
114
  };
package/src/config.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  // biome-ignore lint/performance/noBarrelFile: this facade intentionally preserves the public config module contract.
2
- export { parseArgs } from "./config/args.js";
2
+ export { parseArgs } from "./config/args.ts";
3
3
  export {
4
4
  advisorAutoLoopGateRef,
5
5
  advisorBlockOnBlockedRef,
@@ -68,21 +68,21 @@ export {
68
68
  showUsageFooterRef,
69
69
  simpleModeRef,
70
70
  splitRef,
71
- } from "./config/state.js";
72
- export type { SaveConfigOptions } from "./config/storage.js";
71
+ } from "./config/state.ts";
72
+ export type { SaveConfigOptions } from "./config/storage.ts";
73
73
  export {
74
74
  configPaths,
75
75
  loadConfig,
76
76
  resetConfigCache,
77
77
  saveConfig,
78
78
  saveGlobalOutcomeLogging,
79
- } from "./config/storage.js";
79
+ } from "./config/storage.ts";
80
80
  export type {
81
81
  AdvisorConfig,
82
82
  AdvisorToolPolicies,
83
83
  AdvisorToolPolicy,
84
84
  GateFailureMode,
85
- } from "./config/types.js";
85
+ } from "./config/types.ts";
86
86
  export {
87
87
  ADVISOR_TOOL_POLICIES,
88
88
  DEFAULT_ADVISOR_GIT_CONTEXT_MAX_CHARS,
@@ -91,7 +91,7 @@ export {
91
91
  DEFAULT_CONTEXT_MAX_CHARS,
92
92
  GATE_FAILURE_MODES,
93
93
  MAX_CONTEXT_MAX_CHARS,
94
- } from "./config/types.js";
94
+ } from "./config/types.ts";
95
95
  export {
96
96
  isValidAdvisorToolPolicies,
97
97
  isValidContextMaxChars,
@@ -101,4 +101,4 @@ export {
101
101
  isValidToolResultMaxBytes,
102
102
  isValidToolResultMaxLines,
103
103
  validateConfig,
104
- } from "./config/validation.js";
104
+ } from "./config/validation.ts";
@@ -0,0 +1,30 @@
1
+ /** Shared untyped-content inspection helpers. */
2
+
3
+ export type RecordValue = Record<string, unknown>;
4
+
5
+ export const isRecord = (value: unknown): value is RecordValue =>
6
+ Boolean(value) && typeof value === "object" && !Array.isArray(value);
7
+
8
+ export const byteLength = (value: string) => Buffer.byteLength(value, "utf8");
9
+
10
+ export const contentParts = (content: unknown): unknown[] => {
11
+ if (typeof content === "string") {
12
+ return [content];
13
+ }
14
+ return Array.isArray(content) ? content : [];
15
+ };
16
+
17
+ /** Longest UTF-8 prefix of `value` within `maxBytes`, in linear time. */
18
+ export const capUtf8Bytes = (value: string, maxBytes: number): string => {
19
+ let result = "";
20
+ let used = 0;
21
+ for (const character of value) {
22
+ const characterBytes = byteLength(character);
23
+ if (used + characterBytes > maxBytes) {
24
+ break;
25
+ }
26
+ result += character;
27
+ used += characterBytes;
28
+ }
29
+ return result;
30
+ };
@@ -4,24 +4,11 @@ import {
4
4
  advisorToolPoliciesRef,
5
5
  advisorToolResultMaxBytesRef,
6
6
  advisorToolResultMaxLinesRef,
7
- } from "./config/state.js";
8
- import type { AdvisorToolPolicies } from "./config/types.js";
9
- import {
10
- DEFAULT_ADVISOR_TOOL_RESULT_MAX_BYTES,
11
- DEFAULT_ADVISOR_TOOL_RESULT_MAX_LINES,
12
- } from "./config/types.js";
13
-
14
- type RecordValue = Record<string, unknown>;
15
-
16
- const isRecord = (value: unknown): value is RecordValue =>
17
- Boolean(value) && typeof value === "object";
18
-
19
- const contentParts = (content: unknown): unknown[] => {
20
- if (typeof content === "string") {
21
- return [content];
22
- }
23
- return Array.isArray(content) ? content : [];
24
- };
7
+ } from "./config/state.ts";
8
+ import type { AdvisorToolPolicies } from "./config/types.ts";
9
+ import { contentParts, isRecord, type RecordValue } from "./content-utils.ts";
10
+ import { redactSecrets } from "./redaction.ts";
11
+ import { capToolResult } from "./tool-result-cap.ts";
25
12
 
26
13
  const textFromPart = (part: unknown): string => {
27
14
  if (typeof part === "string") {
@@ -36,153 +23,6 @@ const textFromPart = (part: unknown): string => {
36
23
  export const textFrom = (content: unknown): string =>
37
24
  contentParts(content).map(textFromPart).join("\n").trim();
38
25
 
39
- const byteLength = (value: string) => Buffer.byteLength(value, "utf8");
40
-
41
- const REDACTION_MARKER = "[REDACTED SECRET]";
42
- const PEM_BEGIN_PATTERN = /-----BEGIN(?: [A-Z0-9]+)? PRIVATE KEY-----/gi;
43
- const PEM_END_PATTERN = /-----END(?: [A-Z0-9]+)? PRIVATE KEY-----/i;
44
- const SECRET_PATTERNS = [
45
- /-----BEGIN(?: [A-Z0-9]+)? PRIVATE KEY-----[\s\S]*?-----END(?: [A-Z0-9]+)? PRIVATE KEY-----/gi,
46
- /\bBearer\s+[A-Za-z0-9._~+/=-]{8,}/gi,
47
- /\b(?:api[_-]?key|token|secret|password|passwd)\s*[:=]\s*(?:"[^"]*"|'[^']*'|[^\s"'&,;)}\]]+)/gi,
48
- /([a-z][a-z0-9+.-]*:\/\/)[^\s/@:]+:[^\s/@]+@/gi,
49
- /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/g,
50
- /\b(?:aws_secret_access_key|aws_session_token)\s*[:=]\s*[^\s"'&,;)}\]]+/gi,
51
- ] as const;
52
-
53
- const redactUnterminatedPem = (value: string): string => {
54
- const begins = [...value.matchAll(PEM_BEGIN_PATTERN)];
55
- const lastBegin = begins.at(-1);
56
- if (lastBegin?.index === undefined) {
57
- return value;
58
- }
59
- const hasEnd = PEM_END_PATTERN.test(
60
- value.slice(lastBegin.index + lastBegin[0].length)
61
- );
62
- return hasEnd
63
- ? value
64
- : `${value.slice(0, lastBegin.index)}${REDACTION_MARKER}`;
65
- };
66
-
67
- /** Redacts common credential forms locally; it is not a data-classification system. */
68
- export const redactSecrets = (value: string): string => {
69
- let redacted = redactUnterminatedPem(value);
70
- for (const pattern of SECRET_PATTERNS) {
71
- redacted = redacted.replace(pattern, (_match, scheme) =>
72
- typeof scheme === "string"
73
- ? `${scheme}${REDACTION_MARKER}@`
74
- : REDACTION_MARKER
75
- );
76
- }
77
- return redacted;
78
- };
79
-
80
- /** Redacts before a byte-safe cap so no secret fragment survives truncation. */
81
- export const redactAndCapText = (
82
- value: string,
83
- maxBytes: number,
84
- redact = true
85
- ): string => {
86
- const source = redact ? redactSecrets(value) : value;
87
- let result = "";
88
- for (const character of source) {
89
- if (byteLength(result + character) > maxBytes) {
90
- break;
91
- }
92
- result += character;
93
- }
94
- return result;
95
- };
96
-
97
- export interface ToolResultTruncation {
98
- content: string;
99
- omittedLines: number;
100
- totalBytes: number;
101
- totalLines: number;
102
- truncated: boolean;
103
- }
104
-
105
- export const capToolResult = (
106
- value: string,
107
- maxLines = DEFAULT_ADVISOR_TOOL_RESULT_MAX_LINES,
108
- maxBytes = DEFAULT_ADVISOR_TOOL_RESULT_MAX_BYTES
109
- ): ToolResultTruncation => {
110
- const lines = value.split("\n");
111
- const totalLines = lines.length;
112
- const totalBytes = byteLength(value);
113
- if ((maxLines === 0 || maxBytes === 0) && value.length > 0) {
114
- return {
115
- content: "[Tool result omitted: configured limit is zero]",
116
- omittedLines: totalLines,
117
- totalBytes,
118
- totalLines,
119
- truncated: true,
120
- };
121
- }
122
- if (totalLines <= maxLines && totalBytes <= maxBytes) {
123
- return {
124
- content: value,
125
- omittedLines: 0,
126
- totalBytes,
127
- totalLines,
128
- truncated: false,
129
- };
130
- }
131
-
132
- const marker = "[... omitted tool-result section ...]";
133
- const markerBytes = byteLength(marker);
134
- if (maxBytes < markerBytes || maxLines === 1) {
135
- const content = [...marker].reduce(
136
- (result, character) =>
137
- byteLength(result + character) <= maxBytes
138
- ? result + character
139
- : result,
140
- ""
141
- );
142
- return {
143
- content,
144
- omittedLines: totalLines,
145
- totalBytes,
146
- totalLines,
147
- truncated: true,
148
- };
149
- }
150
- const headCount = Math.floor((maxLines - 1) / 2);
151
- const tailCount = maxLines - 1 - headCount;
152
- const collect = (
153
- candidates: string[],
154
- maxEntries: number,
155
- maxContentBytes: number
156
- ) => {
157
- const selected: string[] = [];
158
- let used = 0;
159
- for (const line of candidates.slice(0, maxEntries)) {
160
- const next = used + byteLength(line) + (selected.length ? 1 : 0);
161
- if (next > maxContentBytes) {
162
- break;
163
- }
164
- selected.push(line);
165
- used = next;
166
- }
167
- return selected;
168
- };
169
- const availableBytes = maxBytes - markerBytes - 2;
170
- const head = collect(lines, headCount, Math.floor(availableBytes / 2));
171
- const tail = collect(
172
- lines.slice(Math.max(head.length, lines.length - tailCount)),
173
- tailCount,
174
- availableBytes - byteLength(head.join("\n"))
175
- );
176
- const content = [...head, marker, ...tail].join("\n");
177
- return {
178
- content,
179
- omittedLines: Math.max(0, totalLines - head.length - tail.length),
180
- totalBytes,
181
- totalLines,
182
- truncated: true,
183
- };
184
- };
185
-
186
26
  const assistantEntry = (
187
27
  message: RecordValue,
188
28
  policies: AdvisorToolPolicies,
@@ -284,6 +124,9 @@ export const conversationEntry = (
284
124
  }
285
125
  };
286
126
 
127
+ const omissionMarker = (omitted: number) =>
128
+ `[Older context omitted: ${omitted} complete entr${omitted === 1 ? "y" : "ies"}]`;
129
+
287
130
  const selectRecentEntries = (entries: string[], maxChars: number): string => {
288
131
  const separator = "\n\n";
289
132
  const joined = entries.join(separator);
@@ -304,19 +147,21 @@ const selectRecentEntries = (entries: string[], maxChars: number): string => {
304
147
  const entry = entries[index];
305
148
  const candidateCount = selected.length + 1;
306
149
  const omitted = entries.length - candidateCount;
307
- const marker = `[Older context omitted: ${omitted} complete entr${omitted === 1 ? "y" : "ies"}]`;
308
150
  const candidateLength =
309
151
  selectedLength +
310
152
  entry.length +
311
153
  (selected.length > 0 ? separator.length : 0);
312
- if (marker.length + separator.length + candidateLength > maxChars) {
154
+ if (
155
+ omissionMarker(omitted).length + separator.length + candidateLength >
156
+ maxChars
157
+ ) {
313
158
  break;
314
159
  }
315
160
  selected.unshift(entry);
316
161
  selectedLength = candidateLength;
317
162
  }
318
163
  const omitted = entries.length - Math.max(1, selected.length);
319
- const marker = `[Older context omitted: ${omitted} complete entr${omitted === 1 ? "y" : "ies"}]`;
164
+ const marker = omissionMarker(omitted);
320
165
  if (selected.length > 0) {
321
166
  return `${marker}${separator}${selected.join(separator)}`;
322
167
  }
package/src/git.ts CHANGED
@@ -16,17 +16,14 @@ const LEVEL_RANK: Record<GitContextLevel, number> = {
16
16
  summary: 1,
17
17
  };
18
18
 
19
- /**
20
- * The Executor may request no more repository context than the user configured.
21
- * A model cannot widen its own disclosure allowance.
22
- */
19
+ /** The Executor may narrow but never widen the user's disclosure allowance. */
23
20
  export const clampGitContextLevel = (
24
21
  requested: GitContextLevel,
25
22
  allowed: GitContextLevel
26
23
  ): GitContextLevel =>
27
24
  LEVEL_RANK[requested] <= LEVEL_RANK[allowed] ? requested : allowed;
28
25
 
29
- export type GitContextStatus =
26
+ type GitContextStatus =
30
27
  | "disabled"
31
28
  | "no-changes"
32
29
  | "collected"
@@ -77,11 +74,7 @@ const GIT_MAX_BUFFER = 16 * 1024 * 1024;
77
74
 
78
75
  export type GitRunner = (args: string[], cwd: string) => string;
79
76
 
80
- /**
81
- * Collection runs several git commands. A per-command timeout alone would let a
82
- * pathological repository block for the sum of them, so the budget is shared:
83
- * each command may use only the time remaining before the overall deadline.
84
- */
77
+ /** Shares one deadline across commands so a pathological repository cannot block for the sum of per-command timeouts. */
85
78
  const deadlineRunner = (): GitRunner => {
86
79
  const expiresAt = Date.now() + GIT_TOTAL_TIMEOUT_MS;
87
80
  return (args, cwd) => {
@@ -112,14 +105,7 @@ const diffBase = (run: GitRunner, cwd: string): string => {
112
105
  }
113
106
  };
114
107
 
115
- /**
116
- * Collects working-tree changes relative to HEAD, covering staged and unstaged
117
- * work. Untracked files are reported by name only and never by content.
118
- *
119
- * `summary` discloses file names, change status, and line counts. It must not
120
- * include diff hunk headers: git derives those from surrounding file content,
121
- * so they can reproduce a secret from a line the change never touched.
122
- */
108
+ /** Collects staged and unstaged changes vs HEAD; summary omits diff hunk headers because git derives them from untouched lines that can reproduce secrets. */
123
109
  export const collectGitContext = (
124
110
  cwd: string,
125
111
  level: GitContextLevel,
package/src/herdr.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  import net from "node:net";
2
- import { getAdvisorSettings } from "./config/state.js";
3
- import { redactSecrets } from "./conversation.js";
2
+ import { getAdvisorSettings } from "./config/state.ts";
3
+ import { redactSecrets } from "./redaction.ts";
4
4
 
5
5
  // Keep the JSON-RPC method components separate from Socket's URL-string heuristic.
6
- const HERDR_NOTIFICATION_METHOD = `${"notification"}.${"show"}`;
6
+ const HERDR_NOTIFICATION_METHOD = "notification.show";
7
7
 
8
8
  const SOURCE = "pi-advisor:advisor-activity";
9
9
  const BLOCK_SOURCE = "pi-advisor:advisor-block";
@@ -38,13 +38,11 @@ export interface HerdrNotificationRequest {
38
38
  sound: "request";
39
39
  };
40
40
  }
41
- export type HerdrRequest = HerdrMetadataRequest | HerdrNotificationRequest;
41
+ type HerdrRequest = HerdrMetadataRequest | HerdrNotificationRequest;
42
42
  type Report = (request: HerdrRequest) => void;
43
43
 
44
- // Herdr only accepts semantic state from the pane's lifecycle authority, which
45
- // for pi is herdr's own herdr-agent-state extension (source "herdr:pi"). Socket
46
- // reports from any other source are dropped, so the blocked state has to be
47
- // signalled through the in-process pi event bus that integration listens on.
44
+ // Herdr drops socket reports from other sources, so the blocked state must be
45
+ // signalled through the in-process pi event bus its integration listens on.
48
46
  type BlockedEmitter = (active: boolean, label: string) => void;
49
47
  let emitBlocked: BlockedEmitter | undefined;
50
48