@the-open-engine/zeroshot 6.37.0 → 6.38.1

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 (78) hide show
  1. package/cli/lib/setup-scanner-worker.js +2 -9
  2. package/lib/agent-cli-provider/adapters/claude-recovery.d.ts.map +1 -1
  3. package/lib/agent-cli-provider/adapters/claude-recovery.js +17 -3
  4. package/lib/agent-cli-provider/adapters/claude-recovery.js.map +1 -1
  5. package/lib/agent-cli-provider/adapters/codex.d.ts.map +1 -1
  6. package/lib/agent-cli-provider/adapters/codex.js +3 -1
  7. package/lib/agent-cli-provider/adapters/codex.js.map +1 -1
  8. package/lib/agent-cli-provider/adapters/index.d.ts.map +1 -1
  9. package/lib/agent-cli-provider/adapters/index.js +7 -0
  10. package/lib/agent-cli-provider/adapters/index.js.map +1 -1
  11. package/lib/agent-cli-provider/adapters/pi.d.ts.map +1 -1
  12. package/lib/agent-cli-provider/adapters/pi.js +86 -97
  13. package/lib/agent-cli-provider/adapters/pi.js.map +1 -1
  14. package/lib/agent-cli-provider/contract-parse.d.ts.map +1 -1
  15. package/lib/agent-cli-provider/contract-parse.js +5 -1
  16. package/lib/agent-cli-provider/contract-parse.js.map +1 -1
  17. package/lib/agent-cli-provider/invoke-evidence.d.ts.map +1 -1
  18. package/lib/agent-cli-provider/invoke-evidence.js +8 -1
  19. package/lib/agent-cli-provider/invoke-evidence.js.map +1 -1
  20. package/lib/agent-cli-provider/pi/credentials.d.ts +2 -0
  21. package/lib/agent-cli-provider/pi/credentials.d.ts.map +1 -0
  22. package/lib/agent-cli-provider/pi/credentials.js +51 -0
  23. package/lib/agent-cli-provider/pi/credentials.js.map +1 -0
  24. package/lib/agent-cli-provider/pi/events.d.ts +5 -0
  25. package/lib/agent-cli-provider/pi/events.d.ts.map +1 -0
  26. package/lib/agent-cli-provider/pi/events.js +124 -0
  27. package/lib/agent-cli-provider/pi/events.js.map +1 -0
  28. package/lib/agent-cli-provider/pi/protocol-state.d.ts +6 -0
  29. package/lib/agent-cli-provider/pi/protocol-state.d.ts.map +1 -0
  30. package/lib/agent-cli-provider/pi/protocol-state.js +138 -0
  31. package/lib/agent-cli-provider/pi/protocol-state.js.map +1 -0
  32. package/lib/agent-cli-provider/pi/release.d.ts +4 -0
  33. package/lib/agent-cli-provider/pi/release.d.ts.map +1 -0
  34. package/lib/agent-cli-provider/pi/release.js +7 -0
  35. package/lib/agent-cli-provider/pi/release.js.map +1 -0
  36. package/lib/agent-cli-provider/probe-availability.d.ts +5 -0
  37. package/lib/agent-cli-provider/probe-availability.d.ts.map +1 -0
  38. package/lib/agent-cli-provider/probe-availability.js +11 -0
  39. package/lib/agent-cli-provider/probe-availability.js.map +1 -0
  40. package/lib/agent-cli-provider/provider-registry.d.ts +13 -9
  41. package/lib/agent-cli-provider/provider-registry.d.ts.map +1 -1
  42. package/lib/agent-cli-provider/provider-registry.js +42 -8
  43. package/lib/agent-cli-provider/provider-registry.js.map +1 -1
  44. package/lib/agent-cli-provider/single-agent-runtime.d.ts.map +1 -1
  45. package/lib/agent-cli-provider/single-agent-runtime.js +15 -5
  46. package/lib/agent-cli-provider/single-agent-runtime.js.map +1 -1
  47. package/lib/agent-cli-provider/types.d.ts +3 -0
  48. package/lib/agent-cli-provider/types.d.ts.map +1 -1
  49. package/lib/agent-cli-provider/types.js.map +1 -1
  50. package/lib/cluster-worker/engine-adapter.js +9 -1
  51. package/lib/docker-config.js +6 -1
  52. package/lib/provider-credential-path.js +36 -0
  53. package/lib/start-cluster.js +8 -0
  54. package/npm-shrinkwrap.json +2 -2
  55. package/package.json +1 -1
  56. package/src/agent/agent-task-executor.js +79 -20
  57. package/src/agent/output-extraction.js +115 -0
  58. package/src/agent/pi-terminal-lifecycle.js +242 -0
  59. package/src/agent/provider-control-plane.js +26 -0
  60. package/src/agent/provider-terminal-failure.js +49 -24
  61. package/src/agent-cli-provider/adapters/claude-recovery.ts +15 -2
  62. package/src/agent-cli-provider/adapters/codex.ts +3 -1
  63. package/src/agent-cli-provider/adapters/index.ts +6 -0
  64. package/src/agent-cli-provider/adapters/pi.ts +105 -125
  65. package/src/agent-cli-provider/contract-parse.ts +8 -1
  66. package/src/agent-cli-provider/invoke-evidence.ts +11 -1
  67. package/src/agent-cli-provider/pi/credentials.ts +47 -0
  68. package/src/agent-cli-provider/pi/events.ts +148 -0
  69. package/src/agent-cli-provider/pi/protocol-state.ts +163 -0
  70. package/src/agent-cli-provider/pi/release.ts +4 -0
  71. package/src/agent-cli-provider/probe-availability.ts +19 -0
  72. package/src/agent-cli-provider/provider-registry.ts +48 -9
  73. package/src/agent-cli-provider/single-agent-runtime.ts +27 -6
  74. package/src/agent-cli-provider/types.ts +3 -0
  75. package/src/config-validator.js +1 -3
  76. package/src/isolation-manager.js +43 -29
  77. package/src/orchestrator.js +33 -1
  78. package/task-lib/watcher-output-runtime.js +16 -5
@@ -0,0 +1,148 @@
1
+ import {
2
+ emitAssistantSnapshot,
3
+ parseAssistantDelta,
4
+ parseToolExecutionEnd,
5
+ parseToolExecutionStart,
6
+ parseToolExecutionUpdate,
7
+ resetAssistantSnapshot,
8
+ } from '../assistant-stream';
9
+ import { getRecord, getString, isRecord, tryParseJson } from '../json';
10
+ import type { OutputEvent, ProviderParserState } from '../types';
11
+ import {
12
+ invalidatePiStream,
13
+ rememberPiAssistant,
14
+ rememberPiUsage,
15
+ settlePiStream,
16
+ } from './protocol-state';
17
+
18
+ const IGNORED_EVENT_TYPES = new Set([
19
+ 'session',
20
+ 'agent_start',
21
+ 'agent_end',
22
+ 'turn_start',
23
+ 'queue_update',
24
+ 'compaction_start',
25
+ 'compaction_end',
26
+ 'auto_retry_start',
27
+ 'auto_retry_end',
28
+ ]);
29
+ const settledStates = new WeakSet<ProviderParserState>();
30
+
31
+ export function createPiParserState(): ProviderParserState {
32
+ return {
33
+ provider: 'pi',
34
+ lastToolId: undefined,
35
+ lastAssistantText: '',
36
+ lastAssistantThinking: '',
37
+ };
38
+ }
39
+
40
+ function parseMessageEvent(
41
+ type: string,
42
+ event: Record<string, unknown>,
43
+ state: ProviderParserState
44
+ ): readonly OutputEvent[] | OutputEvent | null | undefined {
45
+ if (type === 'message_start') {
46
+ resetAssistantSnapshot(getRecord(event, 'message'), state);
47
+ return null;
48
+ }
49
+ if (type === 'message_update') {
50
+ const assistantMessageEvent = getRecord(event, 'assistantMessageEvent');
51
+ if (assistantMessageEvent !== null) {
52
+ const assistantEvent = parseAssistantDelta(assistantMessageEvent, state);
53
+ if (assistantEvent !== null) return assistantEvent;
54
+ }
55
+ const message = getRecord(event, 'message');
56
+ return message === null ? null : emitAssistantSnapshot(message, state);
57
+ }
58
+ if (type !== 'message_end') return undefined;
59
+
60
+ const message = getRecord(event, 'message');
61
+ if (
62
+ message !== null &&
63
+ getString(message, 'role') === 'toolResult' &&
64
+ Object.hasOwn(message, 'usage')
65
+ ) {
66
+ rememberPiUsage(getRecord(message, 'usage'), state, 'Pi tool usage');
67
+ }
68
+ rememberPiAssistant(message, state);
69
+ return message === null || getString(message, 'role') !== 'assistant'
70
+ ? null
71
+ : emitAssistantSnapshot(message, state);
72
+ }
73
+
74
+ function parseToolEvent(
75
+ type: string,
76
+ event: Record<string, unknown>,
77
+ state: ProviderParserState
78
+ ): OutputEvent | null | undefined {
79
+ if (type === 'tool_execution_start') return parseToolExecutionStart(event, state);
80
+ if (type === 'tool_execution_update') return parseToolExecutionUpdate(event, state);
81
+ if (type === 'tool_execution_end') return parseToolExecutionEnd(event, state);
82
+ return undefined;
83
+ }
84
+
85
+ function parseKnownPiEvent(
86
+ type: string,
87
+ parsed: Record<string, unknown>,
88
+ state: ProviderParserState
89
+ ): readonly OutputEvent[] | OutputEvent | null {
90
+ if (type === 'compaction_end') {
91
+ const result = getRecord(parsed, 'result');
92
+ if (result !== null && Object.hasOwn(result, 'usage')) {
93
+ rememberPiUsage(getRecord(result, 'usage'), state, 'Pi compaction usage');
94
+ }
95
+ return null;
96
+ }
97
+ if (type === 'entry_appended') {
98
+ const entry = getRecord(parsed, 'entry');
99
+ if (
100
+ entry !== null &&
101
+ getString(entry, 'type') === 'branch_summary' &&
102
+ Object.hasOwn(entry, 'usage')
103
+ ) {
104
+ rememberPiUsage(getRecord(entry, 'usage'), state, 'Pi branch summary usage');
105
+ }
106
+ return null;
107
+ }
108
+ if (IGNORED_EVENT_TYPES.has(type)) return null;
109
+ const messageEvent = parseMessageEvent(type, parsed, state);
110
+ if (messageEvent !== undefined) return messageEvent;
111
+ const toolEvent = parseToolEvent(type, parsed, state);
112
+ if (toolEvent !== undefined) return toolEvent;
113
+ if (type === 'turn_end') {
114
+ const message = getRecord(parsed, 'message');
115
+ return message === null ? [] : emitAssistantSnapshot(message, state);
116
+ }
117
+ if (type !== 'agent_settled') return null;
118
+ settledStates.add(state);
119
+ return settlePiStream(state);
120
+ }
121
+
122
+ export function parsePiEvent(
123
+ line: string,
124
+ state: ProviderParserState
125
+ ): readonly OutputEvent[] | OutputEvent | null {
126
+ if (line.trim() && settledStates.has(state)) {
127
+ invalidatePiStream(state, 'stdout contained output after agent_settled.');
128
+ return settlePiStream(state);
129
+ }
130
+ const parsed = tryParseJson(line);
131
+ if (!isRecord(parsed)) {
132
+ if (line.trim()) invalidatePiStream(state, 'stdout contained malformed JSON.');
133
+ return null;
134
+ }
135
+
136
+ const type = getString(parsed, 'type');
137
+ if (type === null) {
138
+ invalidatePiStream(state, 'an event was missing its type.');
139
+ return null;
140
+ }
141
+ return parseKnownPiEvent(type, parsed, state);
142
+ }
143
+
144
+ export function finishPiParsing(state: ProviderParserState): OutputEvent | null {
145
+ if (settledStates.has(state)) return null;
146
+ invalidatePiStream(state, 'stdout ended before agent_settled.');
147
+ return settlePiStream(state);
148
+ }
@@ -0,0 +1,163 @@
1
+ import { parseTurnEndResult } from '../assistant-stream';
2
+ import { getNumber, getRecord, getString } from '../json';
3
+ import type { ProviderParserState, ResultEvent } from '../types';
4
+
5
+ const STOP_REASONS = new Set(['stop', 'length', 'toolUse', 'error', 'aborted', 'deferred']);
6
+ const REQUIRED_USAGE_FIELDS = [
7
+ 'input',
8
+ 'output',
9
+ 'cacheRead',
10
+ 'cacheWrite',
11
+ 'totalTokens',
12
+ ] as const;
13
+ const OPTIONAL_USAGE_FIELDS = ['cacheWrite1h', 'reasoning'] as const;
14
+ const COST_FIELDS = ['input', 'output', 'cacheRead', 'cacheWrite', 'total'] as const;
15
+
16
+ interface PiProtocolState {
17
+ latestAssistant?: Record<string, unknown>;
18
+ invalid?: string;
19
+ usage: Record<string, number>;
20
+ cost: Record<string, number>;
21
+ }
22
+
23
+ const protocolStates = new WeakMap<ProviderParserState, PiProtocolState>();
24
+
25
+ function protocolState(state: ProviderParserState): PiProtocolState {
26
+ const current = protocolStates.get(state);
27
+ if (current !== undefined) return current;
28
+ const created = { usage: {}, cost: {} };
29
+ protocolStates.set(state, created);
30
+ return created;
31
+ }
32
+
33
+ export function invalidatePiStream(state: ProviderParserState, error: string): void {
34
+ protocolState(state).invalid = error;
35
+ }
36
+
37
+ function validateUsage(usage: Record<string, unknown>, label: string): string | null {
38
+ for (const field of REQUIRED_USAGE_FIELDS) {
39
+ if (getNumber(usage, field) === null) return `${label}.${field} is missing.`;
40
+ }
41
+ for (const field of OPTIONAL_USAGE_FIELDS) {
42
+ if (usage[field] !== undefined && getNumber(usage, field) === null) {
43
+ return `${label}.${field} is invalid.`;
44
+ }
45
+ }
46
+ const cost = getRecord(usage, 'cost');
47
+ if (cost === null) return `${label}.cost is missing.`;
48
+ for (const field of COST_FIELDS) {
49
+ if (getNumber(cost, field) === null) return `${label}.cost.${field} is missing.`;
50
+ }
51
+ return null;
52
+ }
53
+
54
+ function validateAssistant(message: Record<string, unknown>): string | null {
55
+ if (!Array.isArray(message.content)) return 'Pi assistant message content is missing.';
56
+ const stopReason = getString(message, 'stopReason');
57
+ if (stopReason === null || !STOP_REASONS.has(stopReason)) {
58
+ return 'Pi assistant message stopReason is missing or unsupported.';
59
+ }
60
+ const usage = getRecord(message, 'usage');
61
+ return usage === null
62
+ ? 'Pi assistant message usage is missing.'
63
+ : validateUsage(usage, 'Pi assistant usage');
64
+ }
65
+
66
+ function addNumbers(
67
+ target: Record<string, number>,
68
+ source: Record<string, unknown>,
69
+ fields: readonly string[]
70
+ ): void {
71
+ for (const field of fields) {
72
+ const value = getNumber(source, field);
73
+ if (value !== null) target[field] = (target[field] ?? 0) + value;
74
+ }
75
+ }
76
+
77
+ export function rememberPiAssistant(
78
+ message: Record<string, unknown> | null,
79
+ state: ProviderParserState
80
+ ): void {
81
+ if (message === null) return invalidatePiStream(state, 'Pi message_end was missing its message.');
82
+ const role = getString(message, 'role');
83
+ if (role === null) return invalidatePiStream(state, 'Pi message_end was missing its role.');
84
+ if (role !== 'assistant') return;
85
+ const error = validateAssistant(message);
86
+ if (error !== null) return invalidatePiStream(state, error);
87
+ const current = protocolState(state);
88
+ current.latestAssistant = message;
89
+ rememberPiUsage(getRecord(message, 'usage'), state, 'Pi assistant usage');
90
+ }
91
+
92
+ export function rememberPiUsage(
93
+ usage: Record<string, unknown> | null,
94
+ state: ProviderParserState,
95
+ label: string
96
+ ): void {
97
+ if (usage === null) return invalidatePiStream(state, `${label} is invalid.`);
98
+ const error = validateUsage(usage, label);
99
+ if (error !== null) return invalidatePiStream(state, error);
100
+ const current = protocolState(state);
101
+ addNumbers(current.usage, usage, [...REQUIRED_USAGE_FIELDS, ...OPTIONAL_USAGE_FIELDS]);
102
+ const cost = getRecord(usage, 'cost');
103
+ if (cost !== null) addNumbers(current.cost, cost, COST_FIELDS);
104
+ }
105
+
106
+ function usageResult(
107
+ current: PiProtocolState
108
+ ): Pick<
109
+ ResultEvent,
110
+ | 'inputTokens'
111
+ | 'outputTokens'
112
+ | 'cacheReadInputTokens'
113
+ | 'cacheCreationInputTokens'
114
+ | 'cost'
115
+ | 'modelUsage'
116
+ > {
117
+ const cost = Object.keys(current.cost).length === 0 ? null : current.cost;
118
+ return {
119
+ inputTokens: current.usage.input ?? 0,
120
+ outputTokens: current.usage.output ?? 0,
121
+ cacheReadInputTokens: current.usage.cacheRead ?? 0,
122
+ cacheCreationInputTokens: current.usage.cacheWrite ?? 0,
123
+ cost: cost?.total ?? null,
124
+ modelUsage: { ...current.usage, ...(cost === null ? {} : { cost }) },
125
+ };
126
+ }
127
+
128
+ function failedResult(error: string, current: PiProtocolState): ResultEvent {
129
+ return {
130
+ type: 'result',
131
+ success: false,
132
+ result: null,
133
+ error,
134
+ ...usageResult(current),
135
+ };
136
+ }
137
+
138
+ export function settlePiStream(state: ProviderParserState): ResultEvent {
139
+ const current = protocolState(state);
140
+ if (current.invalid) {
141
+ return failedResult(`Invalid Pi JSON stream: ${current.invalid}`, current);
142
+ }
143
+ if (!current.latestAssistant) {
144
+ return failedResult('Pi settled without an assistant message_end.', current);
145
+ }
146
+ const parsedResult = parseTurnEndResult(
147
+ { message: current.latestAssistant },
148
+ { failureMessage: 'Pi turn failed' }
149
+ );
150
+ const result =
151
+ getString(current.latestAssistant, 'stopReason') === 'deferred'
152
+ ? {
153
+ ...parsedResult,
154
+ success: false,
155
+ result: null,
156
+ error: 'Pi turn deferred without completing.',
157
+ }
158
+ : parsedResult;
159
+ return {
160
+ ...result,
161
+ ...usageResult(current),
162
+ };
163
+ }
@@ -0,0 +1,4 @@
1
+ export const PI_SUPPORTED_VERSION = '0.84.1' as const;
2
+ export const PI_PACKAGE_NAME = '@earendil-works/pi-coding-agent' as const;
3
+ export const PI_INSTALL_COMMAND =
4
+ `npm install -g --ignore-scripts ${PI_PACKAGE_NAME}@${PI_SUPPORTED_VERSION}` as const;
@@ -0,0 +1,19 @@
1
+ import type { ProviderCliFeatures } from './types';
2
+
3
+ type AvailabilityProbe = 'command' | 'help-or-version' | 'supported-version' | undefined;
4
+
5
+ export function runtimeProbeIsAvailable(
6
+ availabilityProbe: AvailabilityProbe,
7
+ evidenceAvailable: boolean | undefined,
8
+ helpText: string,
9
+ versionText: string,
10
+ capabilities: ProviderCliFeatures
11
+ ): boolean {
12
+ const probeAvailable =
13
+ evidenceAvailable ??
14
+ (availabilityProbe === 'command' ? true : Boolean(helpText || versionText));
15
+ const versionSupported =
16
+ availabilityProbe !== 'supported-version' ||
17
+ ('versionMatches' in capabilities && capabilities.versionMatches === true);
18
+ return probeAvailable && versionSupported;
19
+ }
@@ -18,6 +18,7 @@ import {
18
18
  OMP_INSTALL_COMMAND,
19
19
  } from './omp/release';
20
20
  import { OMP_SDK_SETTINGS_DEFAULTS, validateOmpSdkSettings } from './omp/sdk-settings';
21
+ import { PI_INSTALL_COMMAND } from './pi/release';
21
22
  import type { OmpSettingsValidationContext } from './omp/sdk-settings';
22
23
  import type { ModelLevel, ProviderAdapter, StructuredOutputRecoveryAdapter } from './types';
23
24
 
@@ -73,6 +74,9 @@ export interface ProviderDocsMetadata {
73
74
 
74
75
  export interface ProviderDockerMountPreset {
75
76
  readonly host: string;
77
+ // Optional host-only config-root override. Its value selects the mount source but is never
78
+ // forwarded into the container, where `container` remains the canonical provider path.
79
+ readonly hostEnv?: string;
76
80
  readonly container: string;
77
81
  readonly readonly: boolean;
78
82
  }
@@ -130,7 +134,7 @@ interface ProviderRegistryEntryBase {
130
134
  settings: Record<string, unknown>,
131
135
  context?: OmpSettingsValidationContext
132
136
  ) => string | null;
133
- readonly availabilityProbe?: 'command' | 'help-or-version';
137
+ readonly availabilityProbe?: 'command' | 'help-or-version' | 'supported-version';
134
138
  readonly docs: ProviderDocsMetadata;
135
139
  readonly docker: ProviderDockerMetadata;
136
140
  readonly defaultLevels: Readonly<{
@@ -186,6 +190,35 @@ const CLAUDE_DOCKER_ENV_PASSTHROUGH = [
186
190
  'CLAUDE_CODE_USE_BEDROCK',
187
191
  ] as const;
188
192
 
193
+ const PI_EXPLICIT_DOCKER_CREDENTIALS = new Set([
194
+ 'AWS_CONTAINER_CREDENTIALS_FULL_URI',
195
+ 'AWS_CONTAINER_CREDENTIALS_RELATIVE_URI',
196
+ 'AWS_PROFILE',
197
+ 'AWS_WEB_IDENTITY_TOKEN_FILE',
198
+ 'GOOGLE_APPLICATION_CREDENTIALS',
199
+ ]);
200
+
201
+ const PI_DOCKER_CONFIGURATION_ENV = [
202
+ 'AWS_BEDROCK_FORCE_CACHE',
203
+ 'AWS_BEDROCK_FORCE_HTTP1',
204
+ 'AWS_BEDROCK_SKIP_AUTH',
205
+ 'AWS_DEFAULT_REGION',
206
+ 'AWS_ENDPOINT_URL_BEDROCK_RUNTIME',
207
+ 'AWS_REGION',
208
+ 'AZURE_OPENAI_API_VERSION',
209
+ 'AZURE_OPENAI_BASE_URL',
210
+ 'AZURE_OPENAI_DEPLOYMENT_NAME_MAP',
211
+ 'AZURE_OPENAI_RESOURCE_NAME',
212
+ 'CLOUDFLARE_ACCOUNT_ID',
213
+ 'CLOUDFLARE_GATEWAY_ID',
214
+ 'GCLOUD_PROJECT',
215
+ 'GOOGLE_CLOUD_LOCATION',
216
+ 'GOOGLE_CLOUD_PROJECT',
217
+ 'KIMI_CODE_OAUTH_HOST',
218
+ 'KIMI_OAUTH_HOST',
219
+ 'PI_CACHE_RETENTION',
220
+ ] as const;
221
+
189
222
  const SPAWN_INVOKE = Object.freeze({ lane: 'spawn' }) as SpawnProviderInvokeSpec;
190
223
  const ACP_STDIO_INVOKE = Object.freeze({
191
224
  lane: 'acp-stdio',
@@ -444,17 +477,17 @@ export const providerRegistry = [
444
477
  binary: 'pi',
445
478
  command: { kind: 'fixed', command: 'pi', args: [] },
446
479
  invoke: SPAWN_INVOKE,
447
- installInstructions: 'npm install -g --ignore-scripts @earendil-works/pi-coding-agent@0.80.3',
480
+ installInstructions: PI_INSTALL_COMMAND,
448
481
  authInstructions: 'pi\n/login',
449
- credentialPaths: ['~/.pi'],
482
+ credentialPaths: ['$PI_CODING_AGENT_DIR/auth.json', '~/.pi/agent/auth.json'],
450
483
  credentialEnvKeys: piAdapter.credentialEnvKeys,
451
484
  settingsFields: [],
452
- availabilityProbe: 'help-or-version',
485
+ availabilityProbe: 'supported-version',
453
486
  capabilities: {
454
487
  ...STANDARD_CAPABILITIES,
455
488
  mcpServers: false,
456
489
  jsonSchema: false,
457
- reasoningEffort: false,
490
+ reasoningEffort: true,
458
491
  },
459
492
  docs: {
460
493
  label: 'Pi',
@@ -462,11 +495,17 @@ export const providerRegistry = [
462
495
  },
463
496
  docker: {
464
497
  mount: {
465
- host: '~/.pi',
466
- container: '$HOME/.pi',
467
- readonly: true,
498
+ host: '~/.pi/agent',
499
+ hostEnv: 'PI_CODING_AGENT_DIR',
500
+ container: '$HOME/.pi/agent',
501
+ readonly: false,
468
502
  },
469
- envPassthrough: [],
503
+ install: PI_INSTALL_COMMAND,
504
+ configRoots: ['$HOME/.pi/agent'],
505
+ envPassthrough: [
506
+ ...piAdapter.credentialEnvKeys.filter((name) => !PI_EXPLICIT_DOCKER_CREDENTIALS.has(name)),
507
+ ...PI_DOCKER_CONFIGURATION_ENV,
508
+ ],
470
509
  },
471
510
  defaultLevels: {
472
511
  min: piAdapter.defaultMinLevel,
@@ -29,6 +29,7 @@ import {
29
29
  supportsProviderCapability,
30
30
  supportsProviderOutputReformatting,
31
31
  } from './provider-registry';
32
+ import { runtimeProbeIsAvailable } from './probe-availability';
32
33
  import type {
33
34
  BuildProviderCommandOptions,
34
35
  CliFeatureOverrides,
@@ -577,6 +578,10 @@ function resolveRuntimeCliFeatures(
577
578
  supportsWebSearch: false,
578
579
  };
579
580
  }
581
+ if (provider === 'pi') {
582
+ const detected = detectRuntimeProviderCliFeatures(provider, runtimeSettings);
583
+ return overrides === undefined ? detected : mergePiFailClosedCliFeatures(detected, overrides);
584
+ }
580
585
  if (getProviderRegistryEntry(provider).invoke.lane === 'acp-stdio') {
581
586
  const detected = detectRuntimeProviderCliFeatures(provider, runtimeSettings);
582
587
  if (overrides === undefined) return detected;
@@ -595,6 +600,18 @@ function resolveRuntimeCliFeatures(
595
600
  };
596
601
  }
597
602
 
603
+ function mergePiFailClosedCliFeatures(
604
+ detected: ProviderCliFeatures,
605
+ overrides: CliFeatureOverrides
606
+ ): CliFeatureOverrides {
607
+ return Object.fromEntries(
608
+ Object.entries(detected).map(([field, value]) => [
609
+ field,
610
+ typeof value === 'boolean' ? value && Reflect.get(overrides, field) !== false : value,
611
+ ])
612
+ );
613
+ }
614
+
598
615
  function mergeAcpFailClosedCliFeatures(
599
616
  detected: ProviderCliFeatures,
600
617
  overrides: CliFeatureOverrides
@@ -679,7 +696,9 @@ export function probeRuntimeProviderCli(
679
696
 
680
697
  const helpText =
681
698
  evidence?.helpText ??
682
- stringResult(getHelpOutputFn(helpCommand.command, helpCommand.args)).trim();
699
+ (adapter.id === 'pi'
700
+ ? ''
701
+ : stringResult(getHelpOutputFn(helpCommand.command, helpCommand.args)).trim());
683
702
  const versionText =
684
703
  evidence?.versionText ??
685
704
  stringResult(
@@ -691,11 +710,13 @@ export function probeRuntimeProviderCli(
691
710
  const capabilities = attestedCliFeatures(adapter, helpText, versionText);
692
711
 
693
712
  return {
694
- available:
695
- evidence?.available ??
696
- (registryEntry.availabilityProbe === 'help-or-version'
697
- ? Boolean(helpText || versionText)
698
- : true),
713
+ available: runtimeProbeIsAvailable(
714
+ registryEntry.availabilityProbe,
715
+ evidence?.available,
716
+ helpText,
717
+ versionText,
718
+ capabilities
719
+ ),
699
720
  helpText,
700
721
  versionText,
701
722
  capabilities,
@@ -158,8 +158,10 @@ export interface OpencodeCliFeatures extends BaseCliFeatures {
158
158
 
159
159
  export interface PiCliFeatures extends BaseCliFeatures {
160
160
  readonly provider: 'pi';
161
+ readonly versionMatches: boolean;
161
162
  readonly supportsJsonMode: boolean;
162
163
  readonly supportsModel: boolean;
164
+ readonly supportsThinking: boolean;
163
165
  readonly supportsNoSession: boolean;
164
166
  readonly supportsNoExtensions: boolean;
165
167
  readonly supportsNoSkills: boolean;
@@ -573,6 +575,7 @@ export interface ProviderAdapter {
573
575
  buildCommand(context: string, options?: BuildProviderCommandOptions): CommandSpec;
574
576
  extractSessionId?(line: string): string | null;
575
577
  parseEvent(line: string, state: ProviderParserState): ProviderParseResult;
578
+ finishParsing?(state: ProviderParserState): ProviderParseResult;
576
579
  createParserState(): ProviderParserState;
577
580
  resolveModelSpec(level: ModelLevel, overrides?: LevelOverrides): ResolvedModelSpec;
578
581
  validateModelId(modelId: string | null | undefined): string | null | undefined;
@@ -2058,9 +2058,7 @@ function validateProviderSettings(provider, providerSettings) {
2058
2058
  providerModule.resolveModelSpec(level, { [level]: override });
2059
2059
  }
2060
2060
  if (override?.reasoningEffort && !providerSupportsCapability(provider, 'reasoningEffort')) {
2061
- throw new Error(
2062
- `reasoningEffort overrides are only supported for Claude, Codex, and Opencode`
2063
- );
2061
+ throw new Error(`Provider "${provider}" does not support reasoningEffort overrides`);
2064
2062
  }
2065
2063
  if (override?.reasoningEffort && !REASONING_EFFORTS.includes(override.reasoningEffort)) {
2066
2064
  throw new Error(
@@ -30,6 +30,7 @@ const {
30
30
  isUsableEnvValue,
31
31
  validateProviderEnvAuth,
32
32
  } = require('../lib/docker-config');
33
+ const { resolveProviderCredentialPaths } = require('../lib/provider-credential-path');
33
34
  const { getProvider } = require('./providers');
34
35
  const { readRepoSettings } = require('../lib/repo-settings');
35
36
  const { provisionClaudeCredentials } = require('./claude-credentials');
@@ -151,6 +152,34 @@ function expandHomePath(value) {
151
152
  return value.replace(/^~(?=\/|$)/, os.homedir());
152
153
  }
153
154
 
155
+ function appendCredentialMount(plan, mount, explicitContainerPaths, claudeContainerPath) {
156
+ if (mount.container === claudeContainerPath) {
157
+ console.warn(
158
+ `[IsolationManager] Skipping mount for ${mount.host} -> ${mount.container} ` +
159
+ '(Claude config is managed by zeroshot).'
160
+ );
161
+ return;
162
+ }
163
+
164
+ const hostPath = expandHomePath(mount.host);
165
+ let stat;
166
+ try {
167
+ stat = fs.statSync(hostPath);
168
+ } catch {
169
+ return;
170
+ }
171
+ if (hostPath.endsWith('config') && !stat.isFile()) return;
172
+
173
+ plan.mountedHosts.push(hostPath);
174
+ const mountSpec = mount.readonly
175
+ ? `${hostPath}:${mount.container}:ro`
176
+ : `${hostPath}:${mount.container}`;
177
+ plan.args.push('-v', mountSpec);
178
+ if (explicitContainerPaths.has(mount.container)) {
179
+ plan.explicitMountContainerPaths.push(mount.container);
180
+ }
181
+ }
182
+
154
183
  function pathContains(base, target) {
155
184
  const resolvedBase = path.resolve(base);
156
185
  const resolvedTarget = path.resolve(target);
@@ -325,7 +354,6 @@ class IsolationManager {
325
354
  this._removeContainerByName(containerName);
326
355
 
327
356
  workDir = await this._prepareIsolatedWorkspace(clusterId, workDir, reuseExisting);
328
-
329
357
  // The cluster config dir carries Claude credentials (via provisionClaudeCredentials) and the
330
358
  // Claude-specific AskUserQuestion-blocking hook (~/.claude/settings.json PreToolUse), which
331
359
  // only the `claude` CLI reads. Creating and mounting it for every provider — regardless of
@@ -534,33 +562,7 @@ class IsolationManager {
534
562
  const claudeContainerPath = path.posix.join(containerHome, '.claude');
535
563
 
536
564
  for (const mount of mounts) {
537
- if (mount.container === claudeContainerPath) {
538
- console.warn(
539
- `[IsolationManager] Skipping mount for ${mount.host} -> ${mount.container} ` +
540
- '(Claude config is managed by zeroshot).'
541
- );
542
- continue;
543
- }
544
-
545
- const hostPath = expandHomePath(mount.host);
546
-
547
- try {
548
- const stat = fs.statSync(hostPath);
549
- if (hostPath.endsWith('config') && !stat.isFile()) {
550
- continue;
551
- }
552
- } catch {
553
- continue;
554
- }
555
-
556
- const mountSpec = mount.readonly
557
- ? `${hostPath}:${mount.container}:ro`
558
- : `${hostPath}:${mount.container}`;
559
- plan.args.push('-v', mountSpec);
560
- plan.mountedHosts.push(hostPath);
561
- if (explicitContainerPaths.has(mount.container)) {
562
- plan.explicitMountContainerPaths.push(mount.container);
563
- }
565
+ appendCredentialMount(plan, mount, explicitContainerPaths, claudeContainerPath);
564
566
  }
565
567
 
566
568
  const { envToPass, explicitNames } = this._collectDockerEnvVars(
@@ -568,6 +570,16 @@ class IsolationManager {
568
570
  userMountConfig,
569
571
  settings
570
572
  );
573
+ const hostOnlyEnvs = new Set(
574
+ mountConfig
575
+ .filter((preset) => typeof preset === 'string')
576
+ .map((preset) => MOUNT_PRESETS[preset]?.hostEnv)
577
+ .filter((name) => typeof name === 'string')
578
+ );
579
+ for (const hostOnlyEnv of hostOnlyEnvs) {
580
+ delete envToPass[hostOnlyEnv];
581
+ explicitNames.delete(hostOnlyEnv);
582
+ }
571
583
  for (const [key, value] of Object.entries(envToPass)) {
572
584
  plan.args.push('-e', `${key}=${value}`);
573
585
  plan.forwardedEnv[key] = value;
@@ -680,7 +692,9 @@ class IsolationManager {
680
692
 
681
693
  // A mount only counts if it carries the secret (credentialInMount !== false).
682
694
  const credentialPaths = provider.getCredentialPaths ? provider.getCredentialPaths() : [];
683
- const expandedCreds = credentialPaths.map((cred) => expandHomePath(cred));
695
+ const expandedCreds = resolveProviderCredentialPaths({ ...metadata, credentialPaths }).filter(
696
+ (credPath) => fs.existsSync(credPath)
697
+ );
684
698
  const hasCredentialMount =
685
699
  docker.credentialInMount !== false &&
686
700
  mountedHosts.some((hostPath) =>