@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.
- package/CHANGELOG.md +23 -0
- package/LICENSE +21 -0
- package/README.md +255 -2
- package/dist/cli.d.ts +8 -0
- package/dist/cli.js +369 -0
- package/dist/contracts/interfaces.d.ts +262 -0
- package/dist/contracts/interfaces.js +1 -0
- package/dist/contracts/policy-defaults.json +61 -0
- package/dist/contracts/relay-next.interfaces.d.ts +160 -0
- package/dist/contracts/relay-next.interfaces.js +1 -0
- package/dist/engine/cards.d.ts +68 -0
- package/dist/engine/cards.js +76 -0
- package/dist/engine/engine.d.ts +178 -0
- package/dist/engine/engine.js +1162 -0
- package/dist/engine/hard-rules.d.ts +53 -0
- package/dist/engine/hard-rules.js +96 -0
- package/dist/engine/semantic.d.ts +49 -0
- package/dist/engine/semantic.js +125 -0
- package/dist/engine/service.d.ts +62 -0
- package/dist/engine/service.js +587 -0
- package/dist/engine/tool-actions.d.ts +35 -0
- package/dist/engine/tool-actions.js +348 -0
- package/dist/engine/widget.d.ts +29 -0
- package/dist/engine/widget.js +52 -0
- package/dist/ipc/client.d.ts +26 -0
- package/dist/ipc/client.js +106 -0
- package/dist/ipc/server.d.ts +78 -0
- package/dist/ipc/server.js +105 -0
- package/dist/jev/client.d.ts +38 -0
- package/dist/jev/client.js +239 -0
- package/dist/jev/consent.d.ts +12 -0
- package/dist/jev/consent.js +37 -0
- package/dist/jev/index.d.ts +9 -0
- package/dist/jev/index.js +9 -0
- package/dist/jev/mock.d.ts +30 -0
- package/dist/jev/mock.js +77 -0
- package/dist/jev/pi-registry.d.ts +66 -0
- package/dist/jev/pi-registry.js +127 -0
- package/dist/jev/questions.d.ts +15 -0
- package/dist/jev/questions.js +76 -0
- package/dist/jev/sanitizer.d.ts +10 -0
- package/dist/jev/sanitizer.js +59 -0
- package/dist/jev/types.d.ts +78 -0
- package/dist/jev/types.js +54 -0
- package/dist/pi-extension.d.ts +123 -0
- package/dist/pi-extension.js +687 -0
- package/dist/relay/managed.d.ts +120 -0
- package/dist/relay/managed.js +482 -0
- package/dist/relay/negotiate.d.ts +39 -0
- package/dist/relay/negotiate.js +112 -0
- package/dist/runtime.d.ts +74 -0
- package/dist/runtime.js +246 -0
- package/dist/source/agent-check/adapter.d.ts +57 -0
- package/dist/source/agent-check/adapter.js +224 -0
- package/dist/source/agent-file/adapter.d.ts +57 -0
- package/dist/source/agent-file/adapter.js +217 -0
- package/dist/source/task-status-v1/adapter.d.ts +36 -0
- package/dist/source/task-status-v1/adapter.js +263 -0
- package/dist/source/task-status-v1/producer.d.ts +81 -0
- package/dist/source/task-status-v1/producer.js +127 -0
- package/dist/storage/lock.d.ts +14 -0
- package/dist/storage/lock.js +66 -0
- package/dist/storage/schema.sql +166 -0
- package/dist/storage/store.d.ts +352 -0
- package/dist/storage/store.js +555 -0
- package/dist/util/clock.d.ts +23 -0
- package/dist/util/clock.js +34 -0
- package/dist/util/ids.d.ts +8 -0
- package/dist/util/ids.js +29 -0
- package/dist/util/result.d.ts +28 -0
- package/dist/util/result.js +54 -0
- package/package.json +101 -4
package/dist/cli.js
ADDED
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* pi-watcher CLI (design 11.2 shell entry point).
|
|
4
|
+
* - qualify: V0 qualification check, produces qualification.json (distinguishes candidate platforms from measured platforms)
|
|
5
|
+
* - doctor: local diagnostics (lock/DB/relay negotiation/panel summary)
|
|
6
|
+
* - producer: runs a real async task-status-v1 fixture producer
|
|
7
|
+
* - demo: V1 display-only end-to-end demo
|
|
8
|
+
*/
|
|
9
|
+
import * as fs from 'node:fs';
|
|
10
|
+
import * as os from 'node:os';
|
|
11
|
+
import * as path from 'node:path';
|
|
12
|
+
import { spawn, execSync } from 'node:child_process';
|
|
13
|
+
import { startRuntime } from './runtime.js';
|
|
14
|
+
import { runScenario, scenarioSteps } from './source/task-status-v1/producer.js';
|
|
15
|
+
import { negotiateRelay } from './relay/negotiate.js';
|
|
16
|
+
function parseArgs(argv) {
|
|
17
|
+
const [cmd, ...rest] = argv;
|
|
18
|
+
const flags = new Map();
|
|
19
|
+
for (let i = 0; i < rest.length; i++) {
|
|
20
|
+
if (rest[i].startsWith('--')) {
|
|
21
|
+
const key = rest[i].slice(2);
|
|
22
|
+
const next = rest[i + 1];
|
|
23
|
+
if (next && !next.startsWith('--')) {
|
|
24
|
+
flags.set(key, next);
|
|
25
|
+
i += 1;
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
flags.set(key, 'true');
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return { cmd: cmd ?? '', flags };
|
|
33
|
+
}
|
|
34
|
+
async function main() {
|
|
35
|
+
const { cmd, flags } = parseArgs(process.argv.slice(2));
|
|
36
|
+
switch (cmd) {
|
|
37
|
+
case 'qualify':
|
|
38
|
+
await cmdQualify(flags);
|
|
39
|
+
break;
|
|
40
|
+
case 'doctor':
|
|
41
|
+
await cmdDoctor(flags);
|
|
42
|
+
break;
|
|
43
|
+
case 'producer':
|
|
44
|
+
await cmdProducer(flags);
|
|
45
|
+
break;
|
|
46
|
+
case 'demo':
|
|
47
|
+
await cmdDemo(flags);
|
|
48
|
+
break;
|
|
49
|
+
case 'relay-setup':
|
|
50
|
+
await cmdRelaySetup(flags);
|
|
51
|
+
break;
|
|
52
|
+
default:
|
|
53
|
+
console.log('usage: pi-watcher <qualify|doctor|producer|demo|relay-setup> [options]');
|
|
54
|
+
console.log(' qualify [--out qualification.json] V0 qualification check, writes evidence file');
|
|
55
|
+
console.log(' doctor [--root DIR] local diagnostics');
|
|
56
|
+
console.log(' producer --dir D --scenario build-success|build-failure [--interval 200] [--run-id run-1]');
|
|
57
|
+
console.log(' demo V1 display-only end-to-end demo');
|
|
58
|
+
console.log(' relay-setup [--root DIR] [--relay-home DIR] [--finalize] relay consumer-side owner setup');
|
|
59
|
+
process.exit(cmd ? 2 : 0);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
async function cmdQualify(flags) {
|
|
63
|
+
const checks = [];
|
|
64
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'pw-qual-'));
|
|
65
|
+
// 1. SQLite WAL/FULL/FK (measured with native better-sqlite3)
|
|
66
|
+
try {
|
|
67
|
+
const Database = (await import('better-sqlite3')).default;
|
|
68
|
+
const db = new Database(path.join(tmp, 'qual.db'));
|
|
69
|
+
const jm = db.pragma('journal_mode = WAL', { simple: true });
|
|
70
|
+
db.pragma('synchronous = FULL');
|
|
71
|
+
db.pragma('foreign_keys = ON');
|
|
72
|
+
const sync = db.pragma('synchronous', { simple: true });
|
|
73
|
+
const fk = db.pragma('foreign_keys', { simple: true });
|
|
74
|
+
db.close();
|
|
75
|
+
checks.push({
|
|
76
|
+
check: 'sqlite-native-wal-full-fk',
|
|
77
|
+
passed: jm === 'wal' && sync === 2 && fk === 1,
|
|
78
|
+
detail: `journal_mode=${jm}, synchronous=${sync} (2=FULL), foreign_keys=${fk}`
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
catch (e) {
|
|
82
|
+
checks.push({ check: 'sqlite-native-wal-full-fk', passed: false, detail: String(e) });
|
|
83
|
+
}
|
|
84
|
+
// 2. flock fixed-inode mutual exclusion (child process holds the lock)
|
|
85
|
+
try {
|
|
86
|
+
const lockPath = path.join(tmp, 'watcher.lock');
|
|
87
|
+
fs.writeFileSync(lockPath, '', 'utf8');
|
|
88
|
+
const holder = `
|
|
89
|
+
const fs = require('node:fs');
|
|
90
|
+
const { flock } = require('fs-ext');
|
|
91
|
+
const fd = fs.openSync(${JSON.stringify(lockPath)}, 'a');
|
|
92
|
+
flock(fd, 'ex', () => {
|
|
93
|
+
console.log('held');
|
|
94
|
+
setTimeout(() => process.exit(0), 4000);
|
|
95
|
+
});
|
|
96
|
+
`;
|
|
97
|
+
const child = spawnNode(['-e', holder]);
|
|
98
|
+
await waitStdout(child, 'held');
|
|
99
|
+
const fsExt = await import('fs-ext');
|
|
100
|
+
const fsMod = await import('node:fs');
|
|
101
|
+
const fd2 = fsMod.openSync(lockPath, 'a');
|
|
102
|
+
const rejected = await new Promise(resolve => {
|
|
103
|
+
fsExt.flock(fd2, 'exnb', (err) => {
|
|
104
|
+
resolve(!!err);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
fsMod.closeSync(fd2);
|
|
108
|
+
child.kill('SIGKILL');
|
|
109
|
+
checks.push({
|
|
110
|
+
check: 'root-flock-exclusive-contention',
|
|
111
|
+
passed: rejected,
|
|
112
|
+
detail: rejected ? 'second exclusive non-blocking flock rejected (EAGAIN)' : 'second flock unexpectedly acquired'
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
catch (e) {
|
|
116
|
+
checks.push({ check: 'root-flock-exclusive-contention', passed: false, detail: String(e) });
|
|
117
|
+
}
|
|
118
|
+
// 3. storage.sql schema constraints (triggers + uniqueness + partial unique indexes)
|
|
119
|
+
try {
|
|
120
|
+
const { WatchStore } = await import('./storage/store.js');
|
|
121
|
+
const store = await WatchStore.open(path.join(tmp, 'root3'), { mode: 'embedded' });
|
|
122
|
+
const violations = [];
|
|
123
|
+
// outbox identity immutable
|
|
124
|
+
try {
|
|
125
|
+
store.transaction(tx => {
|
|
126
|
+
tx.insertWatch({
|
|
127
|
+
watchId: 'wq', generation: 1, missionRevision: 1, controlRevision: 1,
|
|
128
|
+
lifecycle: 'active', health: 'healthy', ownerSession: 's', ownerBindingEpoch: 1,
|
|
129
|
+
spec: { schemaVersion: 1, watchId: 'wq', generation: 1, missionRevision: 1, controlRevision: 1, mode: 'embedded',
|
|
130
|
+
owner: { sessionId: 's', originAnchor: null, bindingEpoch: 1, profileId: 'p' },
|
|
131
|
+
target: { kind: 'run', sourceId: 'src', taskId: 't', runId: 'r', attemptId: 'a' },
|
|
132
|
+
mission: { objective: 'o', scope: 's', checkpointId: 'c', requiredArtifacts: [], requiresChecks: false, businessAcceptance: 'not_required' },
|
|
133
|
+
policy: { transport: 'local-display', semanticMode: 'off', notificationOwner: 'watcher', requestKinds: [], maxRequestsPerEpisode: 1, episodeCooldownMs: 60000, attentionTtlMs: 120000 },
|
|
134
|
+
limits: { pollMinMs: 1000, pollMaxMs: 30000, maxSilenceMs: 60000, maxProbesPerEpisode: 2, maxJudgeRequestsPerDay: 10, expiresAt: new Date(Date.now() + 3600_000).toISOString() } },
|
|
135
|
+
snapshot: { taskState: 'unknown', coverage: { truncated: false, sourceGap: false }, scopeRevision: 0, backoffMs: 0 },
|
|
136
|
+
nextDueAt: Date.now(), now: Date.now()
|
|
137
|
+
});
|
|
138
|
+
tx.insertOutbox({
|
|
139
|
+
eventId: 'ev1', watchId: 'wq', episodeId: null, generation: 1, eventType: 'watcher.result.v1',
|
|
140
|
+
eventBytes: Buffer.from('{}'), eventDigest: 'd', validUntil: Date.now() + 1000, now: Date.now()
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
let immutableRejected = false;
|
|
144
|
+
try {
|
|
145
|
+
store.transaction(tx => {
|
|
146
|
+
tx.setOutboxAdmission('ev1', 'pending', {}, Date.now());
|
|
147
|
+
tx.mutateOutbox?.();
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
immutableRejected = true;
|
|
152
|
+
}
|
|
153
|
+
if (!immutableRejected) {
|
|
154
|
+
// Verify triggers directly with SQL
|
|
155
|
+
const Database = (await import('better-sqlite3')).default;
|
|
156
|
+
const db = new Database(path.join(tmp, 'root3', 'watcher.db'));
|
|
157
|
+
try {
|
|
158
|
+
db.prepare("UPDATE outbox SET event_bytes = ? WHERE event_id = ?").run(Buffer.from('{"x":1}'), 'ev1');
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
immutableRejected = true;
|
|
162
|
+
}
|
|
163
|
+
db.close();
|
|
164
|
+
}
|
|
165
|
+
if (!immutableRejected)
|
|
166
|
+
violations.push('outbox identity trigger did not reject mutation');
|
|
167
|
+
}
|
|
168
|
+
catch (e) {
|
|
169
|
+
violations.push(`setup: ${String(e)}`);
|
|
170
|
+
}
|
|
171
|
+
store.close();
|
|
172
|
+
checks.push({
|
|
173
|
+
check: 'storage-schema-constraints',
|
|
174
|
+
passed: violations.length === 0,
|
|
175
|
+
detail: violations.length === 0 ? 'outbox identity immutability enforced by trigger' : violations.join('; ')
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
catch (e) {
|
|
179
|
+
checks.push({ check: 'storage-schema-constraints', passed: false, detail: String(e) });
|
|
180
|
+
}
|
|
181
|
+
// 4. relay 1.2 feature negotiation
|
|
182
|
+
const negotiation = await negotiateRelay();
|
|
183
|
+
checks.push({
|
|
184
|
+
check: 'relay-1-2-feature-negotiation',
|
|
185
|
+
passed: negotiation.status === 'unavailable' || negotiation.status === 'managed-1-2',
|
|
186
|
+
detail: `status=${negotiation.status}; transport=${negotiation.transport}; ${negotiation.detail}`
|
|
187
|
+
});
|
|
188
|
+
// 5. runtime versions
|
|
189
|
+
checks.push({
|
|
190
|
+
check: 'runtime-versions',
|
|
191
|
+
passed: true,
|
|
192
|
+
detail: `node=${process.versions.node}, platform=${os.platform()}/${os.arch()}`
|
|
193
|
+
});
|
|
194
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
195
|
+
const out = {
|
|
196
|
+
kind: 'V0_QUALIFICATION',
|
|
197
|
+
date: new Date().toISOString(),
|
|
198
|
+
commit: gitHead(),
|
|
199
|
+
machine: `${os.platform()}/${os.arch()} node ${process.versions.node}`,
|
|
200
|
+
candidatePlatform: 'POSIX (darwin) local',
|
|
201
|
+
testedPlatform: `${os.platform()} local`,
|
|
202
|
+
checks,
|
|
203
|
+
passed: checks.every(c => c.passed),
|
|
204
|
+
note: 'V0 qualification only; not product acceptance (see design 14).'
|
|
205
|
+
};
|
|
206
|
+
const outPath = flags.get('out') ?? 'qualification/qualification.json';
|
|
207
|
+
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
208
|
+
fs.writeFileSync(outPath, JSON.stringify(out, null, 2), 'utf8');
|
|
209
|
+
console.log(JSON.stringify(out, null, 2));
|
|
210
|
+
process.exit(out.passed ? 0 : 5);
|
|
211
|
+
}
|
|
212
|
+
function gitHead() {
|
|
213
|
+
try {
|
|
214
|
+
return execSync('git rev-parse HEAD', { cwd: process.cwd(), encoding: 'utf8' }).trim();
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
function spawnNode(args) {
|
|
221
|
+
return spawn(process.execPath, args, { stdio: ['ignore', 'pipe', 'inherit'] });
|
|
222
|
+
}
|
|
223
|
+
async function waitStdout(child, needle, timeoutMs = 5000) {
|
|
224
|
+
await new Promise((resolve, reject) => {
|
|
225
|
+
let buf = '';
|
|
226
|
+
const timer = setTimeout(() => reject(new Error('timeout waiting for holder lock')), timeoutMs);
|
|
227
|
+
child.stdout?.on('data', (chunk) => {
|
|
228
|
+
buf += chunk.toString('utf8');
|
|
229
|
+
if (buf.includes(needle)) {
|
|
230
|
+
clearTimeout(timer);
|
|
231
|
+
resolve();
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
child.on('exit', code => {
|
|
235
|
+
clearTimeout(timer);
|
|
236
|
+
reject(new Error(`holder exited early: ${code}`));
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
// --- doctor ---
|
|
241
|
+
async function cmdDoctor(flags) {
|
|
242
|
+
const root = path.resolve(flags.get('root') ?? './.pi-watcher');
|
|
243
|
+
const negotiation = await negotiateRelay();
|
|
244
|
+
const report = {
|
|
245
|
+
root,
|
|
246
|
+
lockFile: fs.existsSync(path.join(root, 'watcher.lock')),
|
|
247
|
+
dbFile: fs.existsSync(path.join(root, 'watcher.db')),
|
|
248
|
+
relay: negotiation,
|
|
249
|
+
versions: { node: process.versions.node }
|
|
250
|
+
};
|
|
251
|
+
console.log(JSON.stringify(report, null, 2));
|
|
252
|
+
}
|
|
253
|
+
// --- producer ---
|
|
254
|
+
async function cmdProducer(flags) {
|
|
255
|
+
const dir = flags.get('dir');
|
|
256
|
+
const scenario = (flags.get('scenario') ?? 'build-success');
|
|
257
|
+
const interval = parseInt(flags.get('interval') ?? '200', 10);
|
|
258
|
+
const runId = flags.get('run-id') ?? `run-${Date.now()}`;
|
|
259
|
+
if (!dir) {
|
|
260
|
+
console.error('--dir required');
|
|
261
|
+
process.exit(2);
|
|
262
|
+
}
|
|
263
|
+
const producer = await runScenario(dir, { sourceId: 'executor-local', taskId: 'build', runId, attemptId: 'attempt-1' }, scenarioSteps(scenario, interval));
|
|
264
|
+
console.log(JSON.stringify({ done: true, scenario, dir, runId, lastSnapshot: producer.lastSnapshot }, null, 2));
|
|
265
|
+
}
|
|
266
|
+
// --- demo (V1 display-only) ---
|
|
267
|
+
async function cmdDemo(_flags) {
|
|
268
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'pw-demo-'));
|
|
269
|
+
const sourceRoot = path.join(tmp, 'sources');
|
|
270
|
+
const rootDir = path.join(tmp, 'state');
|
|
271
|
+
const runtime = await startRuntime({
|
|
272
|
+
rootDir,
|
|
273
|
+
sourceRoots: new Map([['executor-local', sourceRoot]]),
|
|
274
|
+
requiredCheckIds: new Map([['executor-local', ['unit-tests']]]),
|
|
275
|
+
pollTickMs: 100
|
|
276
|
+
});
|
|
277
|
+
runtime.startLoop();
|
|
278
|
+
const scenarioP = runScenario(sourceRoot, { sourceId: 'executor-local', taskId: 'build', runId: 'run-demo-1', attemptId: 'attempt-1' }, scenarioSteps('build-failure', 150));
|
|
279
|
+
await scenarioP;
|
|
280
|
+
await new Promise(r => setTimeout(r, 700));
|
|
281
|
+
const actor = {
|
|
282
|
+
actorId: 'demo',
|
|
283
|
+
owner: { sessionId: 'demo-session', originAnchor: null, bindingEpoch: 1, profileId: 'demo' },
|
|
284
|
+
profileRevision: 1,
|
|
285
|
+
permitted: new Set()
|
|
286
|
+
};
|
|
287
|
+
const spec = await runtime.service.register(`req-${Date.now()}`, {
|
|
288
|
+
schemaVersion: 1,
|
|
289
|
+
mode: 'embedded',
|
|
290
|
+
target: { kind: 'run', sourceId: 'executor-local', taskId: 'build', runId: 'run-demo-1', attemptId: 'attempt-1' },
|
|
291
|
+
mission: {
|
|
292
|
+
objective: 'Run the existing async build task; observe status and report on failure or deliverable results.',
|
|
293
|
+
scope: 'Observe run-demo-1 only; do not modify, retry, or terminate the build.',
|
|
294
|
+
checkpointId: 'compile-and-test',
|
|
295
|
+
requiredArtifacts: ['artifact-app'],
|
|
296
|
+
requiresChecks: true,
|
|
297
|
+
businessAcceptance: 'host'
|
|
298
|
+
},
|
|
299
|
+
policy: {
|
|
300
|
+
transport: 'local-display',
|
|
301
|
+
semanticMode: 'off',
|
|
302
|
+
notificationOwner: 'watcher',
|
|
303
|
+
requestKinds: ['task.failed', 'task.terminal'],
|
|
304
|
+
maxRequestsPerEpisode: 1,
|
|
305
|
+
episodeCooldownMs: 60000,
|
|
306
|
+
attentionTtlMs: 120000
|
|
307
|
+
},
|
|
308
|
+
limits: {
|
|
309
|
+
pollMinMs: 1000,
|
|
310
|
+
pollMaxMs: 5000,
|
|
311
|
+
maxSilenceMs: 60000,
|
|
312
|
+
maxProbesPerEpisode: 2,
|
|
313
|
+
maxJudgeRequestsPerDay: 10,
|
|
314
|
+
expiresAt: new Date(Date.now() + 3600_000).toISOString()
|
|
315
|
+
}
|
|
316
|
+
}, actor);
|
|
317
|
+
await new Promise(r => setTimeout(r, 1200));
|
|
318
|
+
const listed = await runtime.service.list(undefined, 50, actor);
|
|
319
|
+
const inspected = await runtime.service.inspect(spec.watchId, actor);
|
|
320
|
+
console.log('=== demo register ===');
|
|
321
|
+
console.log(JSON.stringify({ watchId: spec.watchId }, null, 2));
|
|
322
|
+
console.log('=== list ===');
|
|
323
|
+
console.log(JSON.stringify(listed, null, 2));
|
|
324
|
+
console.log('=== inspect ===');
|
|
325
|
+
console.log(JSON.stringify(inspected, null, 2));
|
|
326
|
+
runtime.close();
|
|
327
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
328
|
+
}
|
|
329
|
+
main().catch(e => {
|
|
330
|
+
console.error(e);
|
|
331
|
+
process.exit(5);
|
|
332
|
+
});
|
|
333
|
+
// --- relay-setup (owner side: consumer declaration + invite + audience/scope configuration) ---
|
|
334
|
+
async function cmdRelaySetup(flags) {
|
|
335
|
+
const { WatcherRelaySource } = await import('./relay/managed.js');
|
|
336
|
+
const cwd = process.cwd();
|
|
337
|
+
const rootDir = path.resolve(flags.get('root') ?? path.join(cwd, '.pi-watcher', 'relay'));
|
|
338
|
+
const relayHome = path.resolve(flags.get('relay-home') ?? process.env.PI_RELAY_HOME ?? path.join(os.homedir(), '.pi', 'relay'));
|
|
339
|
+
const finalize = flags.get('finalize') === 'true';
|
|
340
|
+
const relay = await WatcherRelaySource.open(rootDir, { relayHome });
|
|
341
|
+
const decl = relay.ensureConsumerDeclaration();
|
|
342
|
+
console.log(JSON.stringify({
|
|
343
|
+
step: 'consumer-declaration',
|
|
344
|
+
file: decl.file,
|
|
345
|
+
note: 'Pi sessions (with the relay extension) scan <relay-home>/consumers at startup and register the guard'
|
|
346
|
+
}, null, 2));
|
|
347
|
+
if (!finalize) {
|
|
348
|
+
const invite = relay.createInvite();
|
|
349
|
+
console.log(JSON.stringify({
|
|
350
|
+
step: 'invite',
|
|
351
|
+
inviteFile: invite.inviteFile,
|
|
352
|
+
note: 'Have the host-session model call the relay_bindings tool: action=bind, invitePath=<file above> (single-use), then run relay-setup --finalize'
|
|
353
|
+
}, null, 2));
|
|
354
|
+
await relay.close();
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
if (!relay.hasActiveMembership()) {
|
|
358
|
+
console.log(JSON.stringify({
|
|
359
|
+
step: 'finalize-blocked',
|
|
360
|
+
reason: 'no membership yet — run bind first (see invite step), then retry --finalize',
|
|
361
|
+
status: relay.statusJson()
|
|
362
|
+
}, null, 2));
|
|
363
|
+
await relay.close();
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
const prov = relay.provision();
|
|
367
|
+
console.log(JSON.stringify({ step: 'provisioned', ...prov, transportAfter: 'relay' }, null, 2));
|
|
368
|
+
await relay.close();
|
|
369
|
+
}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/** pi-watcher core ports and domain types (engine, store, sources, judge, managed delivery). */
|
|
2
|
+
export type Id = string;
|
|
3
|
+
export type Json = null | boolean | number | string | Json[] | {
|
|
4
|
+
[key: string]: Json;
|
|
5
|
+
};
|
|
6
|
+
export type SemanticMode = 'off' | 'shadow' | 'active';
|
|
7
|
+
export type RunTarget = {
|
|
8
|
+
kind: 'run';
|
|
9
|
+
sourceId: Id;
|
|
10
|
+
taskId: Id;
|
|
11
|
+
runId: Id;
|
|
12
|
+
attemptId: Id; /**
|
|
13
|
+
* decision-delta 2026-09-21 (universal monitor mode, not in design package v0.2.0):
|
|
14
|
+
* agent-declared
|
|
15
|
+
* generic evidence sources. The typed source enum is removed as a product surface (unified-exec deleted);
|
|
16
|
+
* agent-file / agent-check are the only two agent registration surfaces.
|
|
17
|
+
*
|
|
18
|
+
* file: tail a declared file path; an okPattern/failPattern hit in the tail window is the only
|
|
19
|
+
* content terminal-state evidence (fail takes precedence over ok); growth means progress; file missing/rotated -> degraded, non-terminal;
|
|
20
|
+
* having no fd holder is not exit evidence (a writer may open the file per write).
|
|
21
|
+
*/
|
|
22
|
+
file?: {
|
|
23
|
+
path: string;
|
|
24
|
+
okPattern?: string;
|
|
25
|
+
failPattern?: string;
|
|
26
|
+
}; /**
|
|
27
|
+
* check: an agent-declared bounded check command. The watcher runs cmd at a fixed cadence (within timeoutMs,
|
|
28
|
+
* SIGTERM->SIGKILL); a non-zero exit code has three meanings, mapped by the declaration: exit 0 / okPattern ->
|
|
29
|
+
* succeeded; failCodes / failPattern -> failed; 126/127/timeout/spawn error ->
|
|
30
|
+
* unknown+degraded (the check is broken; do not sentence the task to death); any other non-zero -> running (pending).
|
|
31
|
+
* The original cmd text is stored with the spec as evidence -- it must not contain secrets.
|
|
32
|
+
*/
|
|
33
|
+
check?: {
|
|
34
|
+
cmd: string;
|
|
35
|
+
cwd?: string;
|
|
36
|
+
timeoutMs?: number;
|
|
37
|
+
okPattern?: string;
|
|
38
|
+
failPattern?: string;
|
|
39
|
+
failCodes?: number[];
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
export type ChildRef = {
|
|
43
|
+
watchId: Id;
|
|
44
|
+
generation: number;
|
|
45
|
+
};
|
|
46
|
+
export type Target = RunTarget | {
|
|
47
|
+
kind: 'group';
|
|
48
|
+
members: ChildRef[];
|
|
49
|
+
readyWhen: 'all_terminal' | 'all_succeeded';
|
|
50
|
+
} | {
|
|
51
|
+
kind: 'obligation';
|
|
52
|
+
dependencies: ChildRef[];
|
|
53
|
+
readyWhen: 'all_terminal' | 'all_succeeded';
|
|
54
|
+
hostAction: string;
|
|
55
|
+
};
|
|
56
|
+
export type Owner = {
|
|
57
|
+
sessionId: Id;
|
|
58
|
+
originAnchor: string | null;
|
|
59
|
+
bindingEpoch: number;
|
|
60
|
+
profileId: Id;
|
|
61
|
+
};
|
|
62
|
+
export interface WatchSpec {
|
|
63
|
+
schemaVersion: 1;
|
|
64
|
+
watchId: Id;
|
|
65
|
+
generation: number;
|
|
66
|
+
missionRevision: number;
|
|
67
|
+
controlRevision: number;
|
|
68
|
+
mode: 'embedded' | 'service';
|
|
69
|
+
owner: Owner;
|
|
70
|
+
target: Target;
|
|
71
|
+
mission: {
|
|
72
|
+
objective: string;
|
|
73
|
+
scope: string;
|
|
74
|
+
checkpointId: Id;
|
|
75
|
+
requiredArtifacts: Id[];
|
|
76
|
+
requiresChecks: boolean;
|
|
77
|
+
businessAcceptance: 'host' | 'not_required';
|
|
78
|
+
deadlineAt?: string;
|
|
79
|
+
};
|
|
80
|
+
policy: {
|
|
81
|
+
transport: 'local-display' | 'relay';
|
|
82
|
+
semanticMode: SemanticMode;
|
|
83
|
+
notificationOwner: 'watcher' | 'external';
|
|
84
|
+
requestKinds: string[];
|
|
85
|
+
maxRequestsPerEpisode: number;
|
|
86
|
+
episodeCooldownMs: number;
|
|
87
|
+
attentionTtlMs: number;
|
|
88
|
+
};
|
|
89
|
+
limits: {
|
|
90
|
+
pollMinMs: number;
|
|
91
|
+
pollMaxMs: number;
|
|
92
|
+
maxSilenceMs: number;
|
|
93
|
+
maxProbesPerEpisode: number;
|
|
94
|
+
maxJudgeRequestsPerDay: number;
|
|
95
|
+
expiresAt: string;
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
export type RegisterCandidate = Omit<WatchSpec, 'watchId' | 'generation' | 'missionRevision' | 'controlRevision' | 'owner'>;
|
|
99
|
+
export interface ActorContext {
|
|
100
|
+
actorId: Id;
|
|
101
|
+
owner: Owner;
|
|
102
|
+
profileRevision: number;
|
|
103
|
+
permitted: ReadonlySet<string>;
|
|
104
|
+
}
|
|
105
|
+
export interface Basis {
|
|
106
|
+
watchId: Id;
|
|
107
|
+
generation: number;
|
|
108
|
+
missionRevision: number;
|
|
109
|
+
controlRevision: number;
|
|
110
|
+
observationSeq: number;
|
|
111
|
+
windowDigest: string;
|
|
112
|
+
}
|
|
113
|
+
export interface EvidenceRef {
|
|
114
|
+
evidenceId: Id;
|
|
115
|
+
sourceId: Id;
|
|
116
|
+
capturedAt: string;
|
|
117
|
+
sha256: string;
|
|
118
|
+
bytes: number;
|
|
119
|
+
sampled: boolean;
|
|
120
|
+
}
|
|
121
|
+
export interface Observation {
|
|
122
|
+
schemaVersion: 1;
|
|
123
|
+
observationId: Id;
|
|
124
|
+
target: RunTarget;
|
|
125
|
+
sourceSeq: number;
|
|
126
|
+
observedAt: string;
|
|
127
|
+
kind: 'status' | 'heartbeat' | 'log_delta' | 'validation' | 'artifact_manifest' | 'decision_request';
|
|
128
|
+
payload: Json;
|
|
129
|
+
evidenceRefs: EvidenceRef[];
|
|
130
|
+
}
|
|
131
|
+
export interface Probe {
|
|
132
|
+
probeId: Id;
|
|
133
|
+
revision: number;
|
|
134
|
+
target: RunTarget;
|
|
135
|
+
kind: 'read-status' | 'read-log-delta' | 'read-artifact-manifest' | 'read-validation-result';
|
|
136
|
+
scopeId: Id;
|
|
137
|
+
expiresAt: string;
|
|
138
|
+
timeoutMs: number;
|
|
139
|
+
maxBytes: number;
|
|
140
|
+
}
|
|
141
|
+
export interface SourceCapabilities {
|
|
142
|
+
stableAttempt: boolean;
|
|
143
|
+
terminalEvents: boolean;
|
|
144
|
+
incrementalCursor: boolean;
|
|
145
|
+
exclusiveNotificationOwner: boolean;
|
|
146
|
+
}
|
|
147
|
+
export interface SourceAdapter {
|
|
148
|
+
readonly adapterId: string;
|
|
149
|
+
readonly capabilities: SourceCapabilities;
|
|
150
|
+
read(target: RunTarget, cursor: Json | undefined, signal: AbortSignal): Promise<{
|
|
151
|
+
observations: Observation[];
|
|
152
|
+
nextCursor: Json;
|
|
153
|
+
gap: boolean;
|
|
154
|
+
}>;
|
|
155
|
+
probes(target: RunTarget, basis: Basis): Promise<readonly Probe[]>;
|
|
156
|
+
execute(probe: Probe, signal: AbortSignal): Promise<Observation[]>;
|
|
157
|
+
close(): Promise<void>;
|
|
158
|
+
}
|
|
159
|
+
export type QuestionKey = 'meaningful_progress' | 'unresolved_blocker' | 'needs_host_decision' | 'repeating_without_new_information' | 'claim_conflicts_with_evidence' | 'context_sufficient';
|
|
160
|
+
export interface Judgment {
|
|
161
|
+
basis: Basis;
|
|
162
|
+
model: string;
|
|
163
|
+
questionSet: 'watcher-q1';
|
|
164
|
+
probabilities: Record<QuestionKey, number>;
|
|
165
|
+
receivedAt: string;
|
|
166
|
+
inputTokens: number;
|
|
167
|
+
discarded: boolean;
|
|
168
|
+
}
|
|
169
|
+
export interface JudgePort {
|
|
170
|
+
evaluate(basis: Basis, state: Json, signal: AbortSignal): Promise<Judgment>;
|
|
171
|
+
chooseProbe(basis: Basis, state: Json, candidates: readonly Probe[], signal: AbortSignal): Promise<Id | 'none'>;
|
|
172
|
+
/** Optional availability check (e.g. credentials resolved lazily from the host); absent means always ready. */
|
|
173
|
+
ready?(): Promise<{
|
|
174
|
+
ready: boolean;
|
|
175
|
+
reason?: string;
|
|
176
|
+
model?: string;
|
|
177
|
+
}>;
|
|
178
|
+
}
|
|
179
|
+
export type AttentionReason = 'task.failed' | 'task.terminal' | 'deadline.exceeded' | 'decision.required' | 'blocker.unresolved' | 'progress.repeating' | 'claim.conflict' | 'evidence.insufficient' | 'dependency.ready' | 'monitor.degraded' | 'target.changed';
|
|
180
|
+
export interface AttentionEnvelope {
|
|
181
|
+
schemaVersion: 1;
|
|
182
|
+
envelopeId: Id;
|
|
183
|
+
episodeId: Id;
|
|
184
|
+
episodeRevision: number;
|
|
185
|
+
watchId: Id;
|
|
186
|
+
generation: number;
|
|
187
|
+
missionRevision: number;
|
|
188
|
+
controlRevision: number;
|
|
189
|
+
ownerBindingEpoch: number;
|
|
190
|
+
target: Target;
|
|
191
|
+
reasonCode: AttentionReason;
|
|
192
|
+
summary: string;
|
|
193
|
+
evidenceRefs: EvidenceRef[];
|
|
194
|
+
requiredNextStep: 'inspect-current-watch-before-acting';
|
|
195
|
+
occurredAt: string;
|
|
196
|
+
validUntil: string;
|
|
197
|
+
}
|
|
198
|
+
export interface HostAck {
|
|
199
|
+
schemaVersion: 1;
|
|
200
|
+
requestId: Id;
|
|
201
|
+
watchId: Id;
|
|
202
|
+
generation: number;
|
|
203
|
+
episodeId: Id;
|
|
204
|
+
expectedEpisodeRevision: number;
|
|
205
|
+
action: 'received' | 'investigating' | 'defer' | 'resolved' | 'dismiss';
|
|
206
|
+
reason: string;
|
|
207
|
+
until?: string;
|
|
208
|
+
evidenceIds: Id[];
|
|
209
|
+
}
|
|
210
|
+
export interface VerifiedConsumerResponse {
|
|
211
|
+
responseId: Id;
|
|
212
|
+
deliveryRef: Id;
|
|
213
|
+
ownerBindingEpoch: number;
|
|
214
|
+
digest: string;
|
|
215
|
+
body: HostAck;
|
|
216
|
+
}
|
|
217
|
+
export interface ManagedDeliveryPort {
|
|
218
|
+
/** New managed relay adapter. Never fallback to direct Pi injection. */
|
|
219
|
+
/**
|
|
220
|
+
* decision-delta 2026-10-07 (per-watch scope, design 9.6): optional scope the event is
|
|
221
|
+
* captured under. Omitted -> the adapter's source-wide default scope (legacy behaviour).
|
|
222
|
+
*/
|
|
223
|
+
publish(frozenRequestBytes: Uint8Array, scope?: {
|
|
224
|
+
scopeId: Id;
|
|
225
|
+
revision: number;
|
|
226
|
+
}): Promise<import('./relay-next.interfaces').ManagedReceipt>;
|
|
227
|
+
reconcile(eventId: Id): Promise<import('./relay-next.interfaces').ManagedReceipt>;
|
|
228
|
+
advanceScope(operationId: Id, scopeId: Id, expectedRevision: number, nextRevision: number, state: 'active' | 'paused' | 'closed'): Promise<Json>;
|
|
229
|
+
withdraw(operationId: Id, eventId: Id, reason: string): Promise<import('./relay-next.interfaces').WithdrawResult>;
|
|
230
|
+
readResponses(after: number): Promise<{
|
|
231
|
+
cursor: number;
|
|
232
|
+
responses: VerifiedConsumerResponse[];
|
|
233
|
+
resyncRequired: boolean;
|
|
234
|
+
}>;
|
|
235
|
+
confirmApplied(operationId: Id, responseId: Id, result: import('./relay-next.interfaces').ApplicationResult): Promise<void>;
|
|
236
|
+
close(): Promise<void>;
|
|
237
|
+
}
|
|
238
|
+
export interface Clock {
|
|
239
|
+
wallNow(): number;
|
|
240
|
+
monotonicNow(): number;
|
|
241
|
+
}
|
|
242
|
+
export interface Transaction {
|
|
243
|
+
getWatch(watchId: Id): WatchSpec | undefined;
|
|
244
|
+
compareRevision(basis: Basis): boolean;
|
|
245
|
+
persistCommandResult(requestId: Id, digest: string, result: Json): void;
|
|
246
|
+
}
|
|
247
|
+
export interface Store {
|
|
248
|
+
/** Synchronous, bounded transaction body: external I/O is forbidden. */
|
|
249
|
+
transaction<T>(runtimeEpoch: number, body: (tx: Transaction) => T): T;
|
|
250
|
+
close(): void;
|
|
251
|
+
}
|
|
252
|
+
export interface WatchService {
|
|
253
|
+
register(requestId: Id, candidate: RegisterCandidate, actor: ActorContext): Promise<WatchSpec>;
|
|
254
|
+
list(cursor: string | undefined, limit: number, actor: ActorContext): Promise<Json>;
|
|
255
|
+
inspect(watchId: Id, actor: ActorContext): Promise<Json>;
|
|
256
|
+
check(requestId: Id, watchId: Id, expectedControlRevision: number, actor: ActorContext): Promise<{
|
|
257
|
+
inspectionId: Id;
|
|
258
|
+
}>;
|
|
259
|
+
/** Internal response pump only. No independent network ACK endpoint. */
|
|
260
|
+
applyResponse(response: VerifiedConsumerResponse): Promise<import('./relay-next.interfaces').ApplicationResult>;
|
|
261
|
+
control(requestId: Id, watchId: Id, expectedControlRevision: number, action: 'pause' | 'close' | 'resume', reason: string, actor: ActorContext): Promise<Json>;
|
|
262
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"status": "design-initial-values-not-performance-results",
|
|
4
|
+
"semanticMode": "shadow",
|
|
5
|
+
"jevConsentDefault": false,
|
|
6
|
+
"model": "jev-1.13.0",
|
|
7
|
+
"questionSet": "watcher-q1",
|
|
8
|
+
"thresholds": {
|
|
9
|
+
"contextSufficient": 0.75,
|
|
10
|
+
"needsHostDecision": 0.85,
|
|
11
|
+
"unresolvedBlocker": 0.85,
|
|
12
|
+
"repeating": 0.9,
|
|
13
|
+
"claimConflict": 0.85
|
|
14
|
+
},
|
|
15
|
+
"scheduler": {
|
|
16
|
+
"pollMinMs": 30000,
|
|
17
|
+
"pollMaxMs": 300000,
|
|
18
|
+
"maxSilenceMs": 900000,
|
|
19
|
+
"maxJevInFlight": 2,
|
|
20
|
+
"maxProbeInFlight": 4,
|
|
21
|
+
"maxJudgeRequestsPerWatchDay": 200,
|
|
22
|
+
"maxJudgeRequestsPerRootDay": 2000,
|
|
23
|
+
"judgeTimeoutMs": 10000,
|
|
24
|
+
"maxJudgeAttempts": 3
|
|
25
|
+
},
|
|
26
|
+
"capacity": {
|
|
27
|
+
"maxActiveWatches": 32,
|
|
28
|
+
"maxGroupChildren": 16,
|
|
29
|
+
"maxEvidenceEvents": 16,
|
|
30
|
+
"stateMaxBytes": 24576,
|
|
31
|
+
"evidenceMaxBytes": 12288,
|
|
32
|
+
"maxProbesPerInspection": 1,
|
|
33
|
+
"maxProbesPerEpisode": 2,
|
|
34
|
+
"rootSoftBytes": 1073741824,
|
|
35
|
+
"rootHardBytes": 2147483648
|
|
36
|
+
},
|
|
37
|
+
"attention": {
|
|
38
|
+
"transport": "relay",
|
|
39
|
+
"defaultModelMode": "display",
|
|
40
|
+
"episodeCooldownMs": 60000,
|
|
41
|
+
"ttlMs": 120000,
|
|
42
|
+
"resultTtlMs": 86400000,
|
|
43
|
+
"maxRequestsPerEpisode": 1,
|
|
44
|
+
"batchSize": 1
|
|
45
|
+
},
|
|
46
|
+
"retention": {
|
|
47
|
+
"closedEvidenceDays": 7,
|
|
48
|
+
"closedStructuredDays": 30,
|
|
49
|
+
"retainOpenAndUnknown": true
|
|
50
|
+
},
|
|
51
|
+
"relayManaged": {
|
|
52
|
+
"requiredWireMinor": 2,
|
|
53
|
+
"dispatchCooldownMs": 60000,
|
|
54
|
+
"batchSize": 1,
|
|
55
|
+
"sourceScopeProofMaxMs": 1000,
|
|
56
|
+
"consumerGuardTimeoutMs": 1000,
|
|
57
|
+
"maximumEventAgeMs": 86400000,
|
|
58
|
+
"maximumEventTtlMs": 86400000,
|
|
59
|
+
"dedupRetentionDays": 30
|
|
60
|
+
}
|
|
61
|
+
}
|