@maolon/pi-watcher 0.0.0-stage → 0.1.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 (72) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/LICENSE +21 -0
  3. package/README.md +255 -2
  4. package/dist/cli.d.ts +8 -0
  5. package/dist/cli.js +369 -0
  6. package/dist/contracts/interfaces.d.ts +262 -0
  7. package/dist/contracts/interfaces.js +1 -0
  8. package/dist/contracts/policy-defaults.json +61 -0
  9. package/dist/contracts/relay-next.interfaces.d.ts +160 -0
  10. package/dist/contracts/relay-next.interfaces.js +1 -0
  11. package/dist/engine/cards.d.ts +68 -0
  12. package/dist/engine/cards.js +76 -0
  13. package/dist/engine/engine.d.ts +178 -0
  14. package/dist/engine/engine.js +1162 -0
  15. package/dist/engine/hard-rules.d.ts +53 -0
  16. package/dist/engine/hard-rules.js +96 -0
  17. package/dist/engine/semantic.d.ts +49 -0
  18. package/dist/engine/semantic.js +125 -0
  19. package/dist/engine/service.d.ts +62 -0
  20. package/dist/engine/service.js +587 -0
  21. package/dist/engine/tool-actions.d.ts +35 -0
  22. package/dist/engine/tool-actions.js +348 -0
  23. package/dist/engine/widget.d.ts +29 -0
  24. package/dist/engine/widget.js +52 -0
  25. package/dist/ipc/client.d.ts +26 -0
  26. package/dist/ipc/client.js +106 -0
  27. package/dist/ipc/server.d.ts +78 -0
  28. package/dist/ipc/server.js +105 -0
  29. package/dist/jev/client.d.ts +38 -0
  30. package/dist/jev/client.js +239 -0
  31. package/dist/jev/consent.d.ts +12 -0
  32. package/dist/jev/consent.js +37 -0
  33. package/dist/jev/index.d.ts +9 -0
  34. package/dist/jev/index.js +9 -0
  35. package/dist/jev/mock.d.ts +30 -0
  36. package/dist/jev/mock.js +77 -0
  37. package/dist/jev/pi-registry.d.ts +66 -0
  38. package/dist/jev/pi-registry.js +127 -0
  39. package/dist/jev/questions.d.ts +15 -0
  40. package/dist/jev/questions.js +76 -0
  41. package/dist/jev/sanitizer.d.ts +10 -0
  42. package/dist/jev/sanitizer.js +59 -0
  43. package/dist/jev/types.d.ts +78 -0
  44. package/dist/jev/types.js +54 -0
  45. package/dist/pi-extension.d.ts +123 -0
  46. package/dist/pi-extension.js +687 -0
  47. package/dist/relay/managed.d.ts +120 -0
  48. package/dist/relay/managed.js +482 -0
  49. package/dist/relay/negotiate.d.ts +39 -0
  50. package/dist/relay/negotiate.js +112 -0
  51. package/dist/runtime.d.ts +74 -0
  52. package/dist/runtime.js +246 -0
  53. package/dist/source/agent-check/adapter.d.ts +57 -0
  54. package/dist/source/agent-check/adapter.js +224 -0
  55. package/dist/source/agent-file/adapter.d.ts +57 -0
  56. package/dist/source/agent-file/adapter.js +217 -0
  57. package/dist/source/task-status-v1/adapter.d.ts +36 -0
  58. package/dist/source/task-status-v1/adapter.js +263 -0
  59. package/dist/source/task-status-v1/producer.d.ts +81 -0
  60. package/dist/source/task-status-v1/producer.js +127 -0
  61. package/dist/storage/lock.d.ts +14 -0
  62. package/dist/storage/lock.js +66 -0
  63. package/dist/storage/schema.sql +166 -0
  64. package/dist/storage/store.d.ts +352 -0
  65. package/dist/storage/store.js +555 -0
  66. package/dist/util/clock.d.ts +23 -0
  67. package/dist/util/clock.js +34 -0
  68. package/dist/util/ids.d.ts +8 -0
  69. package/dist/util/ids.js +29 -0
  70. package/dist/util/result.d.ts +28 -0
  71. package/dist/util/result.js +54 -0
  72. package/package.json +101 -4
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Shared runtime IPC server (primary side).
3
+ * Multiple pi sessions on the same project root share one embedded runtime: the lock-holding session (primary) serves
4
+ * attached sessions' tool/panel/widget requests on this unix socket. Protocol: line-delimited JSON.
5
+ *
6
+ * Invariant (design 7.2): the whole project still has a single store writer (the only opener under epoch fencing).
7
+ * Attached sessions never open the DB -- all mutations are serialized through the primary.
8
+ */
9
+ import * as net from 'node:net';
10
+ import * as fs from 'node:fs';
11
+ import * as path from 'node:path';
12
+ export function runtimeSocketPath(rootDir) {
13
+ return path.join(rootDir, 'runtime.sock');
14
+ }
15
+ export class RuntimeIpcServer {
16
+ socketPath;
17
+ handlers;
18
+ server = null;
19
+ constructor(socketPath, handlers) {
20
+ this.socketPath = socketPath;
21
+ this.handlers = handlers;
22
+ }
23
+ /**
24
+ * Call only after holding the root flock: any existing socket file is then a leftover from a crashed primary,
25
+ * so unlink is safe (a live primary's socket cannot coexist with us -- flock is exclusive).
26
+ */
27
+ async start() {
28
+ try {
29
+ fs.unlinkSync(this.socketPath);
30
+ }
31
+ catch { /* no leftover */ }
32
+ await new Promise((resolve, reject) => {
33
+ const srv = net.createServer(conn => this.handleConn(conn));
34
+ srv.once('error', reject);
35
+ srv.listen(this.socketPath, () => resolve());
36
+ this.server = srv;
37
+ });
38
+ }
39
+ async close() {
40
+ const srv = this.server;
41
+ this.server = null;
42
+ if (!srv)
43
+ return;
44
+ await new Promise(resolve => {
45
+ srv.close(() => resolve());
46
+ srv.closeAllConnections?.();
47
+ });
48
+ try {
49
+ fs.unlinkSync(this.socketPath);
50
+ }
51
+ catch { /* already removed on close or nonexistent */ }
52
+ }
53
+ handleConn(conn) {
54
+ let buf = '';
55
+ conn.on('data', d => {
56
+ buf += d.toString('utf8');
57
+ let idx;
58
+ while ((idx = buf.indexOf('\n')) >= 0) {
59
+ const line = buf.slice(0, idx);
60
+ buf = buf.slice(idx + 1);
61
+ if (!line.trim())
62
+ continue;
63
+ let req;
64
+ try {
65
+ req = JSON.parse(line);
66
+ }
67
+ catch {
68
+ conn.write(JSON.stringify({ ok: false, error: { code: 'INVALID_SPEC', message: 'malformed IPC line' } }) + '\n');
69
+ continue;
70
+ }
71
+ void this.dispatch(req)
72
+ .then(res => { conn.write(JSON.stringify(res) + '\n'); })
73
+ .catch(() => conn.destroy());
74
+ }
75
+ });
76
+ conn.on('error', () => conn.destroy());
77
+ }
78
+ async dispatch(req) {
79
+ const r = req;
80
+ try {
81
+ if (r?.op === 'tool' && r.actor)
82
+ return await this.handlers.tool((r.params ?? {}), r.actor);
83
+ if (r?.op === 'panel' && r.actor)
84
+ return await this.handlers.panel(r.actor);
85
+ if (r?.op === 'ack' && r.actor) {
86
+ return await this.handlers.ack({
87
+ episodeId: String(r.episodeId ?? ''),
88
+ action: String(r.action ?? ''),
89
+ note: String(r.note ?? ''),
90
+ until: typeof r.until === 'string' ? r.until : undefined,
91
+ requestId: String(r.requestId ?? `ack-ipc-${Date.now()}`)
92
+ }, r.actor);
93
+ }
94
+ if (r?.op === 'widget') {
95
+ const lastSeq = typeof r.lastNotifySeq === 'number' ? r.lastNotifySeq : undefined;
96
+ const ownerSession = typeof r.ownerSession === 'string' && r.ownerSession.length > 0 ? r.ownerSession : undefined;
97
+ return this.handlers.widget(lastSeq, ownerSession);
98
+ }
99
+ return { ok: false, error: { code: 'INVALID_SPEC', message: `unknown IPC op ${String(r?.op)}` } };
100
+ }
101
+ catch (e) {
102
+ return { ok: false, error: { code: 'STORE_UNAVAILABLE', message: e instanceof Error ? e.message : String(e) } };
103
+ }
104
+ }
105
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Production Jev HTTP client implementing JudgePort.
3
+ *
4
+ * Jev is a System 1 BERT-derived discriminator, NOT an autoregressive LLM.
5
+ * Calls official POST /v1/systemone with fixed model jev-1.13.0.
6
+ */
7
+ import type { Id, Json, Basis, Judgment, Probe, JudgePort } from '../contracts/interfaces.js';
8
+ import { type JevClientOptions } from './types.js';
9
+ export declare class JevHttpClient implements JudgePort {
10
+ private readonly apiKey;
11
+ private readonly baseUrl;
12
+ private readonly timeoutMs;
13
+ private readonly maxRetries;
14
+ private readonly fetchFn;
15
+ private _totalRequests;
16
+ private _totalInputTokens;
17
+ private _totalOutputTokens;
18
+ constructor(options?: JevClientOptions);
19
+ get stats(): {
20
+ totalRequests: number;
21
+ totalInputTokens: number;
22
+ totalOutputTokens: number;
23
+ };
24
+ /**
25
+ * Evaluates the given state against the 6 standard questions.
26
+ */
27
+ evaluate(basis: Basis, state: Json, signal?: AbortSignal): Promise<Judgment>;
28
+ /**
29
+ * Selects the single best candidate probe to resolve an evidence gap, or 'none'.
30
+ */
31
+ chooseProbe(basis: Basis, state: Json, candidates: readonly Probe[], signal?: AbortSignal): Promise<Id | 'none'>;
32
+ /**
33
+ * Low-level post to /v1/systemone with retry logic.
34
+ */
35
+ private postSystemOne;
36
+ private calculateBackoff;
37
+ private sleep;
38
+ }
@@ -0,0 +1,239 @@
1
+ /**
2
+ * Production Jev HTTP client implementing JudgePort.
3
+ *
4
+ * Jev is a System 1 BERT-derived discriminator, NOT an autoregressive LLM.
5
+ * Calls official POST /v1/systemone with fixed model jev-1.13.0.
6
+ */
7
+ import { JevAuthenticationError, JevUnprocessableError, JevRateLimitError, JevServerError, JevTimeoutError, JevError } from './types.js';
8
+ import { STANDARD_QUESTIONS, QUESTION_SET_ID, JEV_MODEL_VERSION, buildProbeChoiceQuestion } from './questions.js';
9
+ import { sanitizeJson } from './sanitizer.js';
10
+ const DEFAULT_BASE_URL = 'https://api.typesafe.ai';
11
+ const DEFAULT_TIMEOUT_MS = 10000;
12
+ const DEFAULT_MAX_RETRIES = 2; // Initial try + 2 retries = 3 max attempts (matches policy-defaults)
13
+ export class JevHttpClient {
14
+ apiKey;
15
+ baseUrl;
16
+ timeoutMs;
17
+ maxRetries;
18
+ fetchFn;
19
+ // Stats tracking
20
+ _totalRequests = 0;
21
+ _totalInputTokens = 0;
22
+ _totalOutputTokens = 0;
23
+ constructor(options = {}) {
24
+ const envKey = process.env.JEV_API_KEY || process.env.jev_api_key || '';
25
+ this.apiKey = options.apiKey !== undefined ? options.apiKey : envKey;
26
+ this.baseUrl = (options.baseUrl || process.env.JEV_BASE_URL || DEFAULT_BASE_URL).replace(/\/+$/, '');
27
+ this.timeoutMs = options.timeoutMs ?? (process.env.JEV_TIMEOUT_MS ? parseInt(process.env.JEV_TIMEOUT_MS, 10) : DEFAULT_TIMEOUT_MS);
28
+ this.maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
29
+ this.fetchFn = options.fetchFn ?? globalThis.fetch;
30
+ }
31
+ get stats() {
32
+ return {
33
+ totalRequests: this._totalRequests,
34
+ totalInputTokens: this._totalInputTokens,
35
+ totalOutputTokens: this._totalOutputTokens
36
+ };
37
+ }
38
+ /**
39
+ * Evaluates the given state against the 6 standard questions.
40
+ */
41
+ async evaluate(basis, state, signal) {
42
+ const sanitizedState = (sanitizeJson(state) || {});
43
+ const requestBody = {
44
+ model: JEV_MODEL_VERSION,
45
+ state: sanitizedState,
46
+ questions: STANDARD_QUESTIONS
47
+ };
48
+ const response = await this.postSystemOne(requestBody, signal);
49
+ const probabilities = {};
50
+ for (const key of Object.keys(STANDARD_QUESTIONS)) {
51
+ const answer = response.answers[key];
52
+ if (!answer || answer.type !== 'noul' || typeof answer.noul !== 'number') {
53
+ throw new JevError(`Jev response missing valid noul probability for question '${key}'`);
54
+ }
55
+ // Clamped probability between 0 and 1
56
+ probabilities[key] = Math.max(0, Math.min(1, answer.noul));
57
+ }
58
+ return {
59
+ basis,
60
+ model: response.model || JEV_MODEL_VERSION,
61
+ questionSet: QUESTION_SET_ID,
62
+ probabilities: probabilities,
63
+ receivedAt: new Date().toISOString(),
64
+ inputTokens: response.usage?.input_tokens ?? 0,
65
+ discarded: false
66
+ };
67
+ }
68
+ /**
69
+ * Selects the single best candidate probe to resolve an evidence gap, or 'none'.
70
+ */
71
+ async chooseProbe(basis, state, candidates, signal) {
72
+ if (candidates.length === 0) {
73
+ return 'none';
74
+ }
75
+ const sanitizedState = (sanitizeJson(state) || {});
76
+ const choiceQuestion = buildProbeChoiceQuestion(candidates);
77
+ const requestBody = {
78
+ model: JEV_MODEL_VERSION,
79
+ state: sanitizedState,
80
+ questions: {
81
+ probe_choice: choiceQuestion
82
+ }
83
+ };
84
+ const response = await this.postSystemOne(requestBody, signal);
85
+ const answer = response.answers.probe_choice;
86
+ if (!answer || answer.type !== 'choice' || typeof answer.choice !== 'string') {
87
+ return 'none';
88
+ }
89
+ const validIds = new Set(candidates.map(p => p.probeId));
90
+ if (validIds.has(answer.choice)) {
91
+ return answer.choice;
92
+ }
93
+ return 'none';
94
+ }
95
+ /**
96
+ * Low-level post to /v1/systemone with retry logic.
97
+ */
98
+ async postSystemOne(requestBody, parentSignal) {
99
+ if (!this.apiKey) {
100
+ throw new JevAuthenticationError(401, 'No Jev API key configured. Provide apiKey option or set JEV_API_KEY environment variable.');
101
+ }
102
+ const url = `${this.baseUrl}/v1/systemone`;
103
+ let attempt = 0;
104
+ while (attempt <= this.maxRetries) {
105
+ attempt++;
106
+ if (parentSignal?.aborted) {
107
+ throw parentSignal.reason || new Error('Aborted');
108
+ }
109
+ const timeoutController = new AbortController();
110
+ const timer = setTimeout(() => timeoutController.abort(), this.timeoutMs);
111
+ const combinedAbortHandler = () => {
112
+ timeoutController.abort();
113
+ };
114
+ if (parentSignal) {
115
+ parentSignal.addEventListener('abort', combinedAbortHandler, { once: true });
116
+ }
117
+ try {
118
+ const res = await this.fetchFn(url, {
119
+ method: 'POST',
120
+ headers: {
121
+ 'Content-Type': 'application/json',
122
+ 'Authorization': `Bearer ${this.apiKey}`,
123
+ 'User-Agent': 'pi-watcher/0.1.0'
124
+ },
125
+ body: JSON.stringify(requestBody),
126
+ signal: timeoutController.signal
127
+ });
128
+ clearTimeout(timer);
129
+ if (parentSignal) {
130
+ parentSignal.removeEventListener('abort', combinedAbortHandler);
131
+ }
132
+ if (res.ok) {
133
+ const data = (await res.json());
134
+ this._totalRequests++;
135
+ if (data.usage) {
136
+ this._totalInputTokens += data.usage.input_tokens || 0;
137
+ this._totalOutputTokens += data.usage.output_tokens || 0;
138
+ }
139
+ return data;
140
+ }
141
+ // Non-OK responses
142
+ const status = res.status;
143
+ const errText = await res.text().catch(() => '');
144
+ // 401 / 403: auth failure -> do NOT retry
145
+ if (status === 401 || status === 403) {
146
+ throw new JevAuthenticationError(status, errText);
147
+ }
148
+ // 422: unprocessable request -> do NOT retry
149
+ if (status === 422) {
150
+ let parsed = errText;
151
+ try {
152
+ parsed = JSON.parse(errText);
153
+ }
154
+ catch {
155
+ // Keep text
156
+ }
157
+ throw new JevUnprocessableError(status, errText, parsed);
158
+ }
159
+ // 429: rate limit
160
+ if (status === 429) {
161
+ const retryAfterSec = parseInt(res.headers.get('Retry-After') || '', 10);
162
+ const retryDelayMs = !isNaN(retryAfterSec) && retryAfterSec > 0
163
+ ? retryAfterSec * 1000
164
+ : this.calculateBackoff(attempt);
165
+ if (attempt <= this.maxRetries) {
166
+ await this.sleep(retryDelayMs, parentSignal);
167
+ continue;
168
+ }
169
+ throw new JevRateLimitError(errText, retryDelayMs);
170
+ }
171
+ // 5xx / 529: transient server errors -> retry with jitter
172
+ if (status >= 500) {
173
+ if (attempt <= this.maxRetries) {
174
+ const backoffMs = this.calculateBackoff(attempt);
175
+ await this.sleep(backoffMs, parentSignal);
176
+ continue;
177
+ }
178
+ throw new JevServerError(status, errText);
179
+ }
180
+ // Other client errors (400, 404, etc.) -> no retry
181
+ throw new JevError(`Jev request failed with status ${status}: ${errText}`);
182
+ }
183
+ catch (err) {
184
+ clearTimeout(timer);
185
+ if (parentSignal) {
186
+ parentSignal.removeEventListener('abort', combinedAbortHandler);
187
+ }
188
+ // Rethrow if deliberate abort or non-retryable
189
+ // Any JevError raised from an HTTP response above has already made its retry
190
+ // decision (429/5xx `continue` while attempts remain); 400/404 and other client
191
+ // errors must not be retried. Only network/abort failures fall through to retry.
192
+ if (err instanceof JevError) {
193
+ throw err;
194
+ }
195
+ if (parentSignal?.aborted) {
196
+ throw parentSignal.reason || new Error('Aborted');
197
+ }
198
+ // Timeout checking
199
+ if (timeoutController.signal.aborted && !parentSignal?.aborted) {
200
+ if (attempt <= this.maxRetries) {
201
+ const backoffMs = this.calculateBackoff(attempt);
202
+ await this.sleep(backoffMs, parentSignal);
203
+ continue;
204
+ }
205
+ throw new JevTimeoutError(this.timeoutMs);
206
+ }
207
+ // Network / fetch errors
208
+ if (attempt <= this.maxRetries) {
209
+ const backoffMs = this.calculateBackoff(attempt);
210
+ await this.sleep(backoffMs, parentSignal);
211
+ continue;
212
+ }
213
+ if (err instanceof JevError) {
214
+ throw err;
215
+ }
216
+ throw new JevError(`Jev network failure: ${err instanceof Error ? err.message : String(err)}`);
217
+ }
218
+ }
219
+ throw new JevError('Max retries exceeded');
220
+ }
221
+ calculateBackoff(attempt) {
222
+ // Exponential backoff with random jitter: 200ms * 2^(attempt - 1) + [0..100]ms
223
+ const base = 200 * Math.pow(2, attempt - 1);
224
+ const jitter = Math.floor(Math.random() * 100);
225
+ return base + jitter;
226
+ }
227
+ sleep(ms, signal) {
228
+ return new Promise((resolve, reject) => {
229
+ if (signal?.aborted) {
230
+ return reject(signal.reason || new Error('Aborted'));
231
+ }
232
+ const t = setTimeout(resolve, ms);
233
+ signal?.addEventListener('abort', () => {
234
+ clearTimeout(t);
235
+ reject(signal.reason || new Error('Aborted'));
236
+ }, { once: true });
237
+ });
238
+ }
239
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * User consent for sending sanitized evidence windows to Jev (design 10.2: a consent separate
3
+ * from the watch profile and from relay grants). Granted only by the user: `JEV_CONSENT=1`, or
4
+ * the `/watcher jev consent on` command, which persists it here. Never settable from the tool.
5
+ */
6
+ export declare function consentFilePath(): string;
7
+ /** Where the effective consent comes from. The environment wins over the stored setting. */
8
+ export declare function jevConsentState(): {
9
+ granted: boolean;
10
+ source: 'env' | 'stored' | 'none';
11
+ };
12
+ export declare function setStoredJevConsent(granted: boolean): void;
@@ -0,0 +1,37 @@
1
+ /**
2
+ * User consent for sending sanitized evidence windows to Jev (design 10.2: a consent separate
3
+ * from the watch profile and from relay grants). Granted only by the user: `JEV_CONSENT=1`, or
4
+ * the `/watcher jev consent on` command, which persists it here. Never settable from the tool.
5
+ */
6
+ import * as fs from 'node:fs';
7
+ import * as os from 'node:os';
8
+ import * as path from 'node:path';
9
+ export function consentFilePath() {
10
+ return process.env?.PI_WATCHER_CONFIG ?? path.join(os.homedir(), '.pi', 'agent', 'pi-watcher.json');
11
+ }
12
+ function readConfig() {
13
+ try {
14
+ return JSON.parse(fs.readFileSync(consentFilePath(), 'utf8'));
15
+ }
16
+ catch {
17
+ return {};
18
+ }
19
+ }
20
+ /** Where the effective consent comes from. The environment wins over the stored setting. */
21
+ export function jevConsentState() {
22
+ const env = process.env?.JEV_CONSENT;
23
+ if (env === '1')
24
+ return { granted: true, source: 'env' };
25
+ if (env === '0')
26
+ return { granted: false, source: 'env' };
27
+ const stored = readConfig().jevConsent;
28
+ return stored === true ? { granted: true, source: 'stored' } : { granted: false, source: stored === false ? 'stored' : 'none' };
29
+ }
30
+ export function setStoredJevConsent(granted) {
31
+ const file = consentFilePath();
32
+ fs.mkdirSync(path.dirname(file), { recursive: true });
33
+ const next = { ...readConfig(), jevConsent: granted, jevConsentUpdatedAt: new Date().toISOString() };
34
+ const tmp = `${file}.${process.pid}.tmp`;
35
+ fs.writeFileSync(tmp, JSON.stringify(next, null, 2) + '\n', { mode: 0o600 });
36
+ fs.renameSync(tmp, file);
37
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Jev evaluator module index.
3
+ * Jev is a System 1 BERT-derived classifier for bounded semantic judgment.
4
+ */
5
+ export * from './types.js';
6
+ export * from './questions.js';
7
+ export * from './sanitizer.js';
8
+ export * from './client.js';
9
+ export * from './mock.js';
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Jev evaluator module index.
3
+ * Jev is a System 1 BERT-derived classifier for bounded semantic judgment.
4
+ */
5
+ export * from './types.js';
6
+ export * from './questions.js';
7
+ export * from './sanitizer.js';
8
+ export * from './client.js';
9
+ export * from './mock.js';
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Mock Judge implementation for deterministic offline testing and CI.
3
+ *
4
+ * Implements JudgePort without making outbound network requests.
5
+ * Note: Per IMPLEMENTATION_HANDOFF.md, MockJudge cannot substitute for live model quality acceptance.
6
+ */
7
+ import type { Id, Json, Basis, QuestionKey, Judgment, Probe, JudgePort } from '../contracts/interfaces.js';
8
+ export interface MockJudgeOptions {
9
+ model?: string;
10
+ defaultProbabilities?: Partial<Record<QuestionKey, number>>;
11
+ evaluateFn?: (basis: Basis, state: Json) => Partial<Record<QuestionKey, number>>;
12
+ chooseProbeFn?: (basis: Basis, state: Json, candidates: readonly Probe[]) => Id | 'none';
13
+ probeChoice?: Id | 'none';
14
+ }
15
+ export declare class MockJudge implements JudgePort {
16
+ readonly model: string;
17
+ private probabilities;
18
+ private probeChoice;
19
+ private evaluateFn?;
20
+ private chooseProbeFn?;
21
+ evaluateCallCount: number;
22
+ chooseProbeCallCount: number;
23
+ lastEvaluatedBasis?: Basis;
24
+ lastEvaluatedState?: Json;
25
+ constructor(options?: MockJudgeOptions);
26
+ setProbabilities(probabilities: Partial<Record<QuestionKey, number>>): void;
27
+ setProbeChoice(choice: Id | 'none'): void;
28
+ evaluate(basis: Basis, state: Json, signal?: AbortSignal): Promise<Judgment>;
29
+ chooseProbe(basis: Basis, state: Json, candidates: readonly Probe[], signal?: AbortSignal): Promise<Id | 'none'>;
30
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Mock Judge implementation for deterministic offline testing and CI.
3
+ *
4
+ * Implements JudgePort without making outbound network requests.
5
+ * Note: Per IMPLEMENTATION_HANDOFF.md, MockJudge cannot substitute for live model quality acceptance.
6
+ */
7
+ import { QUESTION_SET_ID, JEV_MODEL_VERSION } from './questions.js';
8
+ const DEFAULT_PROBABILITIES = {
9
+ meaningful_progress: 0.5,
10
+ unresolved_blocker: 0.1,
11
+ needs_host_decision: 0.1,
12
+ repeating_without_new_information: 0.05,
13
+ claim_conflicts_with_evidence: 0.05,
14
+ context_sufficient: 0.95
15
+ };
16
+ export class MockJudge {
17
+ model;
18
+ probabilities;
19
+ probeChoice;
20
+ evaluateFn;
21
+ chooseProbeFn;
22
+ evaluateCallCount = 0;
23
+ chooseProbeCallCount = 0;
24
+ lastEvaluatedBasis;
25
+ lastEvaluatedState;
26
+ constructor(options = {}) {
27
+ this.model = options.model || JEV_MODEL_VERSION;
28
+ this.probabilities = { ...DEFAULT_PROBABILITIES, ...options.defaultProbabilities };
29
+ this.probeChoice = options.probeChoice ?? 'none';
30
+ this.evaluateFn = options.evaluateFn;
31
+ this.chooseProbeFn = options.chooseProbeFn;
32
+ }
33
+ setProbabilities(probabilities) {
34
+ this.probabilities = { ...this.probabilities, ...probabilities };
35
+ }
36
+ setProbeChoice(choice) {
37
+ this.probeChoice = choice;
38
+ }
39
+ async evaluate(basis, state, signal) {
40
+ if (signal?.aborted) {
41
+ throw signal.reason || new Error('Aborted');
42
+ }
43
+ this.evaluateCallCount++;
44
+ this.lastEvaluatedBasis = basis;
45
+ this.lastEvaluatedState = state;
46
+ let dynamicProbabilities = {};
47
+ if (this.evaluateFn) {
48
+ dynamicProbabilities = this.evaluateFn(basis, state);
49
+ }
50
+ const merged = { ...this.probabilities, ...dynamicProbabilities };
51
+ return {
52
+ basis,
53
+ model: this.model,
54
+ questionSet: QUESTION_SET_ID,
55
+ probabilities: merged,
56
+ receivedAt: new Date().toISOString(),
57
+ inputTokens: 256,
58
+ discarded: false
59
+ };
60
+ }
61
+ async chooseProbe(basis, state, candidates, signal) {
62
+ if (signal?.aborted) {
63
+ throw signal.reason || new Error('Aborted');
64
+ }
65
+ this.chooseProbeCallCount++;
66
+ if (this.chooseProbeFn) {
67
+ return this.chooseProbeFn(basis, state, candidates);
68
+ }
69
+ if (this.probeChoice !== 'none') {
70
+ const exists = candidates.some(c => c.probeId === this.probeChoice);
71
+ if (exists) {
72
+ return this.probeChoice;
73
+ }
74
+ }
75
+ return 'none';
76
+ }
77
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Jev through Pi's own model registry (Pi >= 1.1 ships TypeSafe's Jev as a classifier model).
3
+ *
4
+ * Credentials come from Pi, not from this package: `TYPESAFE_API_KEY`, or any provider that
5
+ * serves Jev and is configured in Pi (`/login`, auth.json, provider env vars). The judge resolves
6
+ * an available Jev model lazily on every pass, so a credential added mid-session via `/login`
7
+ * is picked up without a restart.
8
+ *
9
+ * The watcher-q1 questions are `noul` (probability that the true criterion holds); Pi's
10
+ * classifier API expresses the same thing as `bool` questions whose answer carries
11
+ * `probability` = P(true). The probe choice maps to a `choice` question.
12
+ */
13
+ import type { Id, Json, Basis, Judgment, Probe, JudgePort } from '../contracts/interfaces.js';
14
+ /** Structural subset of Pi's ClassifierModel. */
15
+ export interface PiClassifierModel {
16
+ provider: string;
17
+ id: string;
18
+ }
19
+ /** Structural subset of Pi's ClassifierResult. */
20
+ export interface PiClassifierResult {
21
+ provider: string;
22
+ model: string;
23
+ answers: Record<string, {
24
+ type: string;
25
+ probability?: number;
26
+ choice?: string;
27
+ }>;
28
+ usage?: {
29
+ input?: number;
30
+ };
31
+ stopReason: 'stop' | 'error' | 'aborted';
32
+ errorMessage?: string;
33
+ }
34
+ /** Structural subset of Pi's ModelRegistry (`ctx.modelRegistry`). */
35
+ export interface PiClassifierRegistry {
36
+ getAvailableOfType(type: 'classifier', provider?: string): Promise<readonly PiClassifierModel[]>;
37
+ classify(model: PiClassifierModel, context: {
38
+ state: Record<string, Json>;
39
+ questions: Record<string, unknown>;
40
+ }, options?: {
41
+ signal?: AbortSignal;
42
+ }): Promise<PiClassifierResult>;
43
+ }
44
+ /**
45
+ * Jev models Pi lists, in preference order: TypeSafe direct first, then gateways that serve the
46
+ * same model. Override with PI_WATCHER_JEV_MODEL=<provider>/<modelId>.
47
+ */
48
+ export declare const JEV_MODEL_PREFERENCE: ReadonlyArray<readonly [string, string]>;
49
+ export declare class PiRegistryJudge implements JudgePort {
50
+ private readonly registry;
51
+ private readonly override;
52
+ private readonly now;
53
+ private cached;
54
+ constructor(registry: () => PiClassifierRegistry | undefined, override?: string | undefined, now?: () => number);
55
+ /** Picks the first Jev model whose provider has working credentials in Pi. */
56
+ resolveModel(): Promise<PiClassifierModel | null>;
57
+ ready(): Promise<{
58
+ ready: boolean;
59
+ reason?: string;
60
+ model?: string;
61
+ }>;
62
+ evaluate(basis: Basis, state: Json, signal: AbortSignal): Promise<Judgment>;
63
+ chooseProbe(_basis: Basis, state: Json, candidates: readonly Probe[], signal: AbortSignal): Promise<Id | 'none'>;
64
+ private requireModel;
65
+ private classify;
66
+ }