create-principles-disciple 1.146.8 → 1.146.9

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.
@@ -1,9 +1,12 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
- import { sendSuccess, sendError, sendNotFound } from '../utils/response.js';
4
+ import { sendSuccess, sendError, sendNotFound, sendJson } from '../utils/response.js';
5
5
  import { PrinciplesConsoleModel } from '../models/PrinciplesConsoleModel.js';
6
- import { loadLabModules, loadHostCapability, preflightCheck, offlineStudyPass, reportMaterialize, listStudyNames, resolveStudyAccess, resolveStudyRoots, studyStatusPayload, studyReportPayload, materializeConsoleStudy, verifyConsoleOwnership, writableRootProblem, isJsonObject, ExaminerPreflightBlockedError, } from './examiner-executor.js';
6
+ import { ExaminerEvidenceModel } from '../models/ExaminerEvidenceModel.js';
7
+ import { loadLabModules, loadHostCapability, preflightCheck, offlineStudyPass, reportMaterialize, listStudyNames, resolveStudyAccess, resolveStudyRoots, studyReportPayload, materializeConsoleStudy, verifyConsoleOwnership, writableRootProblem, isJsonObject, ExaminerPreflightBlockedError, } from './examiner-executor.js';
8
+ import { listSuiteCatalog, isSuiteKey, gatherProductionSnapshot, deriveStudyRuleCodeFacts, readProductionSnapshot, } from './examiner-study-inputs.js';
9
+ import { appendConsoleRunEventLocked, readConsoleRuns, currentConsoleRun, recoverUnfinishedConsoleRuns, acceptConsoleRun, hostDryRunPass, f2AuthorizationGate, CONSOLE_RUN_SCHEMA, CONSOLE_RUN_MODES, ConsoleRunInProgressError, ConsoleLedgerUnreadableError, studyStatusPayload, assertOfflineAdmissionAllowed, } from './examiner-runs.js';
7
10
  /* eslint-disable @typescript-eslint/max-params -- node:http handler signature (req, res, workspaceDir, subPath) matches every other route */
8
11
  const LAB_ENTRY = path.join('lib', 'examiner-study.mjs');
9
12
  const PRINCIPLE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
@@ -83,6 +86,30 @@ async function loadLab() {
83
86
  return null;
84
87
  }
85
88
  }
89
+ /**
90
+ * v0.10.1 §12.3 restart recovery runs ONCE per process per writable root,
91
+ * lazily on the first examiner request touching studies — a crashed holder's
92
+ * unfinished console run becomes CONSOLE_RUN_ABANDONED (RESULT_UNKNOWN,
93
+ * never retried) before any status/run request can observe or extend it.
94
+ */
95
+ const recoveryInFlight = new Map();
96
+ async function ensureConsoleRunRecovery(roots, labRootDir) {
97
+ const key = roots.writable;
98
+ const pending = recoveryInFlight.get(key);
99
+ if (pending)
100
+ return pending;
101
+ // Cache the IN-FLIGHT promise, not a "done" flag: a transient scan failure
102
+ // (an unavailable studies root) must be retried by the next request instead
103
+ // of leaving a crashed run blocking this study for the process lifetime.
104
+ const attempt = recoverUnfinishedConsoleRuns(roots, labRootDir)
105
+ .then(() => undefined)
106
+ .catch((error) => {
107
+ recoveryInFlight.delete(key);
108
+ logRefusal('run recovery', error);
109
+ });
110
+ recoveryInFlight.set(key, attempt);
111
+ return attempt;
112
+ }
86
113
  function readBody(req) {
87
114
  return new Promise((resolve) => {
88
115
  let raw = '';
@@ -105,27 +132,47 @@ function readBody(req) {
105
132
  });
106
133
  }
107
134
  export async function handleExaminerRoute(req, res, workspaceDir, subPath) {
135
+ const method = req.method ?? 'GET';
136
+ // ── v0.10.1 §4/§5: production evidence aggregate (BEFORE the lab gate —
137
+ // production evidence is a property of the workspace runtime, not of the
138
+ // research engine; an installation without the lab still answers honestly).
139
+ // Segment parsing (no regex): /principles/<charset-validated-id>/evidence.
140
+ const evidencePrefix = '/principles/';
141
+ const evidenceSuffix = '/evidence';
142
+ const evidencePrincipleId = subPath.startsWith(evidencePrefix) && subPath.endsWith(evidenceSuffix)
143
+ ? subPath.slice(evidencePrefix.length, subPath.length - evidenceSuffix.length)
144
+ : null;
145
+ if (evidencePrincipleId !== null && PRINCIPLE_ID_PATTERN.test(evidencePrincipleId) && method === 'GET') {
146
+ const model = new ExaminerEvidenceModel(workspaceDir);
147
+ const evidence = await model.getPrincipleEvidence(evidencePrincipleId);
148
+ if (!evidence) {
149
+ sendError(res, 404, 'principle_not_found', 'the principle was not found in the workspace ledger');
150
+ return;
151
+ }
152
+ sendSuccess(res, { ...evidence });
153
+ return;
154
+ }
108
155
  const lab = await loadLab();
109
156
  if (!lab || !labModules || !labRoot) {
110
157
  sendError(res, 503, 'examiner_lab_unavailable', `the Examiner research engine could not be loaded from this installation: ${labUnavailableReason ?? 'unknown reason'}`);
111
158
  return;
112
159
  }
113
- const method = req.method ?? 'GET';
114
160
  const roots = resolveStudyRoots(labRoot, path.join(workspaceDir, '.pd', 'examiner'));
115
161
  const context = { labRoot, roots };
116
162
  const rootProblem = writableRootProblem(roots);
163
+ await ensureConsoleRunRecovery(roots, labRoot);
117
164
  // GET /overview
118
165
  if (subPath === '/overview' && method === 'GET') {
119
166
  const writable = listStudyNames(roots.writable);
120
167
  const historical = listStudyNames(roots.historical);
121
168
  sendSuccess(res, {
122
- spec: 'SPEC-PD-EXAMINER-001 v0.9',
123
- executionScope: 'offline-stub only — every study uses SYNTHETIC_OFFLINE gold and the deterministic local stub; this panel validates the research contract, never real host behavior',
169
+ spec: 'SPEC-PD-EXAMINER-001 v0.10.1',
170
+ executionScope: 'offline-stub studies validate the research contract; host dry-run checks F2 readiness with ZERO model calls; real host runs require an APPROVED Stage R authorization inside the study and stay refused otherwise',
124
171
  gates: {
125
172
  gate1_challenge_trust: 'BLOCKED — independent human Gold review has not been performed for any Examiner suite (code tests can never substitute)',
126
173
  gate2_behavior_evaluation: 'NOT_READY — requires Gate 1 + real-host authorization',
127
174
  gate3_llm_authoring: 'NOT_STARTED',
128
- gate4_product_integration: 'ACTIVE — the offline research panel (create / run / report) ships with the console; real model calls and real host runs stay refused under this gate',
175
+ gate4_product_integration: 'ACTIVE — the offline research panel (create / run / report) ships with the console; v0.10.1 adds production evidence, host-backed study inputs and the async run contract; real model calls and real host runs stay refused under this gate',
129
176
  },
130
177
  hostInjectionCapability: lab,
131
178
  fullConclusionMinimumLevel: 'CONTEXT_PRESENT',
@@ -149,6 +196,28 @@ export async function handleExaminerRoute(req, res, workspaceDir, subPath) {
149
196
  sendSuccess(res, { principles, note: 'study inputs are read from the workspace ledger (read-only); eligibility filtering happens inside the Examiner contract' });
150
197
  return;
151
198
  }
199
+ // ── v0.10.1 §9: challenge-suite catalog (server-fixed keys, read-only).
200
+ if (subPath === '/suites' && method === 'GET') {
201
+ const suites = await listSuiteCatalog(labRoot);
202
+ sendSuccess(res, { suites, note: 'every suite reuses the lab\u2019s authored calibration case variants; gold is SYNTHETIC_OFFLINE until Gate 1 (independent human Gold review) passes' });
203
+ return;
204
+ }
205
+ // ── v0.10.1 §6.2: OpenClaw host readiness (workspace-level, zero model calls).
206
+ if (subPath === '/hosts/openclaw/readiness' && method === 'GET') {
207
+ sendSuccess(res, {
208
+ host: 'openclaw',
209
+ maxProvableInjectionLevel: lab.openclaw ?? 'SUBMITTED',
210
+ executionModes: ['offline-stub', 'host-dry-run', 'f2-real-host'],
211
+ requirements: [
212
+ { id: 'stage_r_authorization', detail: 'an APPROVED F2-real-host run-authorization.json inside the study, bound to its frozen suite hash (issued by the Owner — never by the console)' },
213
+ { id: 'host_cli', detail: 'the OpenClaw CLI reachable on PATH (verified by the per-study host dry-run local version probe)' },
214
+ { id: 'isolation', detail: 'per-arm round directories creatable inside the writable study root' },
215
+ { id: 'gate1_gold_trust', detail: 'independent human Gold review — currently BLOCKED, so real runs cannot produce formal effect conclusions even when authorized' },
216
+ ],
217
+ note: 'this readiness projection performs no host calls; run the per-study host dry-run for the live checklist',
218
+ });
219
+ return;
220
+ }
152
221
  // POST /studies — create an offline examiner study (identity server-minted)
153
222
  if (subPath === '/studies' && method === 'POST') {
154
223
  if (rootProblem) {
@@ -160,6 +229,9 @@ export async function handleExaminerRoute(req, res, workspaceDir, subPath) {
160
229
  const requestedMode = typeof body.experimentMode === 'string' ? body.experimentMode : '';
161
230
  const repetitionsRaw = Number(body.repetitionsPerCase);
162
231
  const repetitionsPerCase = Number.isInteger(repetitionsRaw) && repetitionsRaw >= 1 && repetitionsRaw <= 5 ? repetitionsRaw : 2;
232
+ // v0.10.1 §9: the suite travels as a CATALOG KEY (server-fixed list);
233
+ // anything unrecognized falls back to the v0.9 default suite.
234
+ const suiteKey = isSuiteKey(body.suiteKey) ? body.suiteKey : 'didactic-pair';
163
235
  if (!PRINCIPLE_ID_PATTERN.test(requestedTarget)) {
164
236
  sendError(res, 400, 'invalid_target_principle', 'targetPrincipleId required (charset-validated principle id; one target Principle per study)');
165
237
  return;
@@ -178,18 +250,35 @@ export async function handleExaminerRoute(req, res, workspaceDir, subPath) {
178
250
  }
179
251
  const isolatedMode = requestedMode === 'isolated_principle';
180
252
  try {
253
+ // v0.10.1 host-backed inputs (SPEC §4/§6/§7): freeze the production
254
+ // snapshot, derive RuleCode facts from RUNTIME activation truth,
255
+ // carry the real pain lineage and compile the selected suite's cases.
256
+ // The study handle is minted inside materializeConsoleStudy, so the
257
+ // suite cases are compiled with per-study ids there via a second pass
258
+ // (suiteCaseDefs is deterministic; the ids are re-minted below).
259
+ const snapshot = await gatherProductionSnapshot(workspaceDir, requestedTarget, 2000);
260
+ const detailOutput = await model.getPrincipleDetail(requestedTarget);
261
+ const painRefs = detailOutput?.principle.derivedFromPainIds ?? [];
181
262
  const created = await materializeConsoleStudy(labModules, labRoot, roots, {
182
263
  isolatedMode,
183
264
  targetIndex,
184
265
  ledgerPrinciples,
185
266
  repetitionsPerCase,
267
+ suiteKey,
268
+ hostBacked: {
269
+ productionSnapshot: { ...snapshot },
270
+ ruleCodeFacts: deriveStudyRuleCodeFacts(snapshot),
271
+ painRefs,
272
+ },
186
273
  });
187
274
  sendSuccess(res, {
188
275
  studyId: created.studyId,
189
276
  targetPrincipleId: created.targetPrincipleId,
190
277
  experimentMode: isolatedMode ? 'isolated_principle' : 'full_library_incremental',
278
+ suiteKey,
191
279
  plannedPairs: created.plannedPairs,
192
280
  manifestHash: created.manifestHash,
281
+ productionSnapshotMarkers: snapshot.experimentVsProduction.markers,
193
282
  note: 'offline study created with SYNTHETIC gold — run preflight, then the offline-stub execution; results demonstrate the contract pipeline, never real host behavior',
194
283
  });
195
284
  }
@@ -201,6 +290,11 @@ export async function handleExaminerRoute(req, res, workspaceDir, subPath) {
201
290
  }
202
291
  // /studies/:studyId[/...]
203
292
  const studyMatch = STUDY_ROUTE_PATTERN.exec(subPath);
293
+ // CodeQL js/user-controlled-bypass (dismissed as false positive): this is a route
294
+ // SELECTOR, not an authorization decision. Every privileged action below
295
+ // runs only after resolveStudyAccess() (server-side enumeration + study-id
296
+ // charset allowlist + canonical boundary + ownership marker); a non-match
297
+ // simply falls through to 404.
204
298
  if (studyMatch && studyMatch[1]) {
205
299
  const rest = studyMatch[2] ?? '';
206
300
  const access = resolveStudyAccess(roots, studyMatch[1]);
@@ -211,7 +305,33 @@ export async function handleExaminerRoute(req, res, workspaceDir, subPath) {
211
305
  const studyDir = access.dir;
212
306
  // GET status
213
307
  if (rest === '' && method === 'GET') {
214
- sendSuccess(res, { ...studyStatusPayload(studyDir), readOnly: access.readOnly, access: access.reason });
308
+ // P1-4B: an unreadable ledger is reported STRUCTURALLY on the read path
309
+ // (rc-9) and refuses runs on the write path — never shown as "no runs".
310
+ let consoleRuns = null;
311
+ let ledgerProblem = null;
312
+ try {
313
+ consoleRuns = readConsoleRuns(roots, studyDir);
314
+ }
315
+ catch (error) {
316
+ if (error instanceof ConsoleLedgerUnreadableError) {
317
+ ledgerProblem = { code: error.code, message: error.message };
318
+ }
319
+ else {
320
+ throw error;
321
+ }
322
+ }
323
+ sendSuccess(res, {
324
+ ...studyStatusPayload(roots, studyDir),
325
+ readOnly: access.readOnly,
326
+ access: access.reason,
327
+ productionSnapshot: readProductionSnapshot(studyDir),
328
+ consoleRuns: {
329
+ runs: consoleRuns === null ? [] : consoleRuns.slice(-10),
330
+ current: consoleRuns === null ? null : currentConsoleRun(consoleRuns),
331
+ asyncModes: CONSOLE_RUN_MODES,
332
+ ledgerProblem,
333
+ },
334
+ });
215
335
  return;
216
336
  }
217
337
  // POST /preflight — the fail-closed gate over the frozen study facts
@@ -224,21 +344,140 @@ export async function handleExaminerRoute(req, res, workspaceDir, subPath) {
224
344
  // P1-1/P1-4: the study must be server-owned and writable, and the SERVER
225
345
  // preflight must pass BEFORE the runner is entered. A blocker returns 409
226
346
  // with no RUN_STARTED event, no reservation and no actor creation.
347
+ // CodeQL js/user-controlled-bypass (dismissed as false positive): route selector. The
348
+ // mode allowlist, access.readOnly refusal and the preflight/Stage R gates
349
+ // below all run before any execution side effect.
227
350
  if (rest === '/run' && method === 'POST') {
228
351
  const body = await readBody(req);
229
352
  // Real execution needs an Owner-approved run-authorization.json plus the
230
- // Stage R gate — this console route NEVER transports a caller-supplied
231
- // mode into the runner; only the offline-stub default is reachable.
232
- if (body.mode !== undefined && body.mode !== 'offline-stub') {
233
- sendError(res, 403, 'real_execution_not_authorized', 'non-offline execution requires explicit Owner authorization (run-authorization.json + Stage R gate) — NOT GRANTED this round');
353
+ // Stage R gate. The reachable modes are fixed: offline-stub (v0.9,
354
+ // synchronous), host-dry-run / f2-real-host (v0.10.1 §12.3 async).
355
+ const requestedMode = typeof body.mode === 'string' ? body.mode : 'offline-stub';
356
+ if (requestedMode !== 'offline-stub' && !CONSOLE_RUN_MODES.includes(requestedMode)) {
357
+ sendError(res, 403, 'real_execution_not_authorized', `unknown execution mode '${requestedMode}' — allowed: offline-stub, host-dry-run, f2-real-host`);
234
358
  return;
235
359
  }
236
360
  if (access.readOnly) {
237
361
  sendError(res, 403, 'study_read_only', `this study cannot be executed from the console: ${access.reason}`);
238
362
  return;
239
363
  }
364
+ // ── v0.10.1 §12.3 async modes (host-dry-run / f2-real-host) ──
365
+ if (requestedMode === 'host-dry-run' || requestedMode === 'f2-real-host') {
366
+ // P1-5: the full Stage R authority contract (schema/stage/binding/
367
+ // window/revocation/probe) is verified BEFORE acceptance; the same
368
+ // authority re-verifies per unit inside the runner for real runs.
369
+ // CodeQL js/user-controlled-bypass (dismissed as false positive): selecting the F2
370
+ // mode ADDS the Stage R gate (it cannot skip one); the other two
371
+ // reachable modes perform no real-host action at all.
372
+ if (requestedMode === 'f2-real-host') {
373
+ const gate = await f2AuthorizationGate(labRoot, studyDir);
374
+ if (!gate.authorized) {
375
+ sendError(res, 403, 'real_execution_not_authorized', `F2 real-host execution was refused: ${gate.reasons.join('; ')}`);
376
+ return;
377
+ }
378
+ }
379
+ // P1-4: read-check-verify-append acceptance happens ATOMICALLY under
380
+ // the existing lab study write lock — two concurrent requests can
381
+ // never both pass. A corrupt/unreadable ledger refuses the run.
382
+ let acceptance;
383
+ try {
384
+ acceptance = await acceptConsoleRun(roots, studyDir, { studyId: access.studyId, mode: requestedMode, labRoot });
385
+ }
386
+ catch (error) {
387
+ if (error instanceof ConsoleRunInProgressError) {
388
+ sendError(res, 409, 'run_in_progress', `console run ${error.runId} (${error.mode}) is still in progress for this study — wait for its terminal event or recover after a restart; never start a second run`, { runId: error.runId, mode: error.mode, acceptedAt: error.acceptedAt });
389
+ return;
390
+ }
391
+ if (error.name === 'OfflineRunInFlightError') {
392
+ sendError(res, 409, 'run_in_progress', 'an offline execution is currently in progress for this study (RUN_STARTED without RUN_FINISHED) — wait for it to settle');
393
+ return;
394
+ }
395
+ if (error.name === 'StudyLockError') {
396
+ sendError(res, 409, 'run_in_progress', 'another writer holds the study lock right now (a concurrent acceptance or the offline runner) — exactly one run may start; retry after the holder settles');
397
+ return;
398
+ }
399
+ if (error instanceof ConsoleLedgerUnreadableError) {
400
+ sendError(res, 500, 'console_ledger_unreadable', `the run was refused because the study event ledger could not be honestly read (${error.code}): ${error.message} — the ledger is never auto-cleared or rebuilt`);
401
+ return;
402
+ }
403
+ logRefusal('console run acceptance failed', error);
404
+ sendError(res, 500, 'run_acceptance_failed', 'the run could not be accepted (details in the console server log)');
405
+ return;
406
+ }
407
+ const { runId, acceptedAt } = acceptance;
408
+ // Detached work: the accepted run settles itself through the study
409
+ // ledger. The promise is intentionally not awaited (§12.3: long work
410
+ // never rides the HTTP request); all failures append terminal events.
411
+ // finishedAt is taken at SETTLEMENT time (P1-4C), never up front.
412
+ void (async () => {
413
+ let terminal;
414
+ try {
415
+ if (requestedMode === 'host-dry-run') {
416
+ const dryRun = await hostDryRunPass({ modules: labModules, labRoot, studyDir, context });
417
+ terminal = {
418
+ type: dryRun.ok ? 'CONSOLE_RUN_FINISHED' : 'CONSOLE_RUN_FAILED',
419
+ schema: CONSOLE_RUN_SCHEMA, runId, studyId: access.studyId,
420
+ mode: 'host-dry-run', acceptedAt, pid: process.pid, owner: 'pd-console',
421
+ finishedAt: new Date().toISOString(),
422
+ outcome: dryRun.ok ? 'DRY_RUN_CHECKS_PASSED' : 'DRY_RUN_CHECKS_FAILED',
423
+ reason: dryRun.ok ? undefined : `failing prerequisites: ${dryRun.checks.filter((entry) => !entry.ok && entry.severity !== 'info').map((entry) => entry.id).join(', ')}`,
424
+ checks: dryRun.checks,
425
+ };
426
+ }
427
+ else {
428
+ // f2-real-host passed the console gate. Real console-driven
429
+ // execution is NOT enabled this round (SPEC §Owner Decision:
430
+ // real model calls require separate Owner authorization); the
431
+ // run settles LOUDLY with zero model calls — never a stub.
432
+ terminal = {
433
+ type: 'CONSOLE_RUN_FAILED', schema: CONSOLE_RUN_SCHEMA, runId, studyId: access.studyId,
434
+ mode: 'f2-real-host', acceptedAt, pid: process.pid, owner: 'pd-console',
435
+ finishedAt: new Date().toISOString(),
436
+ outcome: 'F2_EXECUTION_NOT_ENABLED_THIS_ROUND',
437
+ reason: 'authorization was accepted, but console-driven F2 real execution stays disabled this round; use the lab F2 driver under Owner supervision; zero model calls were made',
438
+ };
439
+ }
440
+ }
441
+ catch (error) {
442
+ logRefusal(`console run ${runId} failed`, error);
443
+ terminal = {
444
+ type: 'CONSOLE_RUN_FAILED', schema: CONSOLE_RUN_SCHEMA, runId, studyId: access.studyId,
445
+ mode: requestedMode, acceptedAt, pid: process.pid, owner: 'pd-console',
446
+ finishedAt: new Date().toISOString(),
447
+ outcome: 'RUN_ERROR', reason: 'the console run failed (details in the console server log)',
448
+ };
449
+ }
450
+ // The terminal event is appended UNDER the study write lock: startup
451
+ // recovery takes the same lock and re-reads the terminal state there,
452
+ // so a settled run can never be turned into ABANDONED afterwards.
453
+ try {
454
+ await appendConsoleRunEventLocked({ labRoot, roots, studyDir }, terminal);
455
+ }
456
+ catch (error) {
457
+ logRefusal(`console run ${runId} terminal event could not be appended`, error);
458
+ }
459
+ })();
460
+ // 202 Accepted — the §12.3 async contract answer.
461
+ sendJson(res, 202, {
462
+ success: true,
463
+ data: {
464
+ studyId: access.studyId,
465
+ runId,
466
+ mode: requestedMode,
467
+ status: 'ACCEPTED',
468
+ note: requestedMode === 'host-dry-run'
469
+ ? 'host dry-run accepted — zero model calls; poll the study status for the console-run checklist'
470
+ : 'F2 real-host run accepted under the Owner authorization — poll the study status; the runner re-verifies the Stage R authority per unit',
471
+ },
472
+ });
473
+ return;
474
+ }
475
+ // ── v0.9 synchronous offline-stub path (unchanged, AC-02 compat) ──
240
476
  try {
241
- const summary = await offlineStudyPass(labModules, studyDir, context);
477
+ // P1-3 (offline→async): the admission check runs INSIDE the lab's study
478
+ // write lock, before RUN_STARTED/reservation/actor. The callback lives
479
+ // here (composition root) so executor↔runs stay acyclic.
480
+ const summary = await offlineStudyPass(labModules, studyDir, context, () => assertOfflineAdmissionAllowed(roots, studyDir));
242
481
  sendSuccess(res, { studyId: access.studyId, units: summary.units.map((unit) => ({ unit: unit.unit, state: unit.state })) });
243
482
  }
244
483
  catch (error) {
@@ -246,6 +485,16 @@ export async function handleExaminerRoute(req, res, workspaceDir, subPath) {
246
485
  // refuses BEFORE RUN_STARTED; the runner re-verifies UNDER the study
247
486
  // lock and refuses on drift discovered after that first check. Either
248
487
  // way: no RUN_STARTED, no reservation, no actor, no attempt file.
488
+ if (error instanceof ConsoleRunInProgressError) {
489
+ sendError(res, 409, 'run_in_progress', 'an execution is already running for this study; no run started, no unit reserved, no actor created', {
490
+ runId: error.runId, mode: error.mode, acceptedAt: error.acceptedAt,
491
+ });
492
+ return;
493
+ }
494
+ if (error instanceof ConsoleLedgerUnreadableError) {
495
+ sendError(res, 500, 'console_ledger_unreadable', 'the study event ledger is unreadable; refusing to start an offline run (fail-closed)', { code: error.code });
496
+ return;
497
+ }
249
498
  if (error instanceof ExaminerPreflightBlockedError) {
250
499
  sendError(res, 409, 'preflight_blocked', 'offline preflight blocked; no run started, no unit reserved, no actor created', {
251
500
  blockers: error.blockers,
@@ -82,6 +82,7 @@ export type ApiResponse<T> = {
82
82
  } | {
83
83
  success: false;
84
84
  error: string;
85
+ code?: string;
85
86
  reason?: string;
86
87
  nextAction?: string;
87
88
  status?: number;
@@ -25,7 +25,13 @@
25
25
  --color-green-500: oklch(72.3% 0.219 149.579);
26
26
  --color-green-600: oklch(62.7% 0.194 149.214);
27
27
  --color-green-700: oklch(52.7% 0.154 150.069);
28
+ --color-emerald-400: oklch(76.5% 0.177 163.223);
29
+ --color-emerald-500: oklch(69.6% 0.17 162.48);
30
+ --color-emerald-700: oklch(50.8% 0.118 165.612);
28
31
  --color-teal-600: oklch(60% 0.118 184.704);
32
+ --color-sky-400: oklch(74.6% 0.16 232.661);
33
+ --color-sky-500: oklch(68.5% 0.169 237.323);
34
+ --color-sky-700: oklch(50% 0.134 242.749);
29
35
  --color-rose-400: oklch(71.2% 0.194 13.428);
30
36
  --color-black: #000;
31
37
  --color-white: #fff;
@@ -464,6 +470,9 @@
464
470
  .mt-\[14px\] {
465
471
  margin-top: 14px;
466
472
  }
473
+ .mr-1 {
474
+ margin-right: var(--spacing);
475
+ }
467
476
  .mr-2 {
468
477
  margin-right: calc(var(--spacing) * 2);
469
478
  }
@@ -569,6 +578,10 @@
569
578
  .table {
570
579
  display: table;
571
580
  }
581
+ .size-3\.5 {
582
+ width: calc(var(--spacing) * 3.5);
583
+ height: calc(var(--spacing) * 3.5);
584
+ }
572
585
  .size-4 {
573
586
  width: calc(var(--spacing) * 4);
574
587
  height: calc(var(--spacing) * 4);
@@ -995,6 +1008,9 @@
995
1008
  .flex-wrap {
996
1009
  flex-wrap: wrap;
997
1010
  }
1011
+ .content-start {
1012
+ align-content: flex-start;
1013
+ }
998
1014
  .items-baseline {
999
1015
  align-items: baseline;
1000
1016
  }
@@ -1118,6 +1134,10 @@
1118
1134
  margin-block-start: calc(14px * var(--tw-space-y-reverse));
1119
1135
  margin-block-end: calc(14px * calc(1 - var(--tw-space-y-reverse)));
1120
1136
  }
1137
+ .gap-x-3 {
1138
+ -moz-column-gap: calc(var(--spacing) * 3);
1139
+ column-gap: calc(var(--spacing) * 3);
1140
+ }
1121
1141
  .gap-x-4 {
1122
1142
  -moz-column-gap: calc(var(--spacing) * 4);
1123
1143
  column-gap: calc(var(--spacing) * 4);
@@ -1264,6 +1284,12 @@
1264
1284
  border-color: color-mix(in oklab, var(--color-amber-400) 50%, transparent);
1265
1285
  }
1266
1286
  }
1287
+ .border-amber-500\/40 {
1288
+ border-color: color-mix(in srgb, oklch(76.9% 0.188 70.08) 40%, transparent);
1289
+ @supports (color: color-mix(in lab, red, red)) {
1290
+ border-color: color-mix(in oklab, var(--color-amber-500) 40%, transparent);
1291
+ }
1292
+ }
1267
1293
  .border-amber\/20 {
1268
1294
  border-color: color-mix(in srgb, #a66a2a 20%, transparent);
1269
1295
  @supports (color: color-mix(in lab, red, red)) {
@@ -1486,6 +1512,12 @@
1486
1512
  border-color: color-mix(in oklab, var(--color-rose-400) 50%, transparent);
1487
1513
  }
1488
1514
  }
1515
+ .border-sky-500\/40 {
1516
+ border-color: color-mix(in srgb, oklch(68.5% 0.169 237.323) 40%, transparent);
1517
+ @supports (color: color-mix(in lab, red, red)) {
1518
+ border-color: color-mix(in oklab, var(--color-sky-500) 40%, transparent);
1519
+ }
1520
+ }
1489
1521
  .border-transparent {
1490
1522
  border-color: transparent;
1491
1523
  }
@@ -1525,6 +1557,18 @@
1525
1557
  .bg-amber-500 {
1526
1558
  background-color: var(--color-amber-500);
1527
1559
  }
1560
+ .bg-amber-500\/10 {
1561
+ background-color: color-mix(in srgb, oklch(76.9% 0.188 70.08) 10%, transparent);
1562
+ @supports (color: color-mix(in lab, red, red)) {
1563
+ background-color: color-mix(in oklab, var(--color-amber-500) 10%, transparent);
1564
+ }
1565
+ }
1566
+ .bg-amber-500\/15 {
1567
+ background-color: color-mix(in srgb, oklch(76.9% 0.188 70.08) 15%, transparent);
1568
+ @supports (color: color-mix(in lab, red, red)) {
1569
+ background-color: color-mix(in oklab, var(--color-amber-500) 15%, transparent);
1570
+ }
1571
+ }
1528
1572
  .bg-amber\/5 {
1529
1573
  background-color: color-mix(in srgb, #a66a2a 5%, transparent);
1530
1574
  @supports (color: color-mix(in lab, red, red)) {
@@ -1582,6 +1626,18 @@
1582
1626
  background-color: color-mix(in oklab, var(--color-destructive) 10%, transparent);
1583
1627
  }
1584
1628
  }
1629
+ .bg-destructive\/15 {
1630
+ background-color: color-mix(in srgb, #8b3a3a 15%, transparent);
1631
+ @supports (color: color-mix(in lab, red, red)) {
1632
+ background-color: color-mix(in oklab, var(--color-destructive) 15%, transparent);
1633
+ }
1634
+ }
1635
+ .bg-emerald-500\/15 {
1636
+ background-color: color-mix(in srgb, oklch(69.6% 0.17 162.48) 15%, transparent);
1637
+ @supports (color: color-mix(in lab, red, red)) {
1638
+ background-color: color-mix(in oklab, var(--color-emerald-500) 15%, transparent);
1639
+ }
1640
+ }
1585
1641
  .bg-gov {
1586
1642
  background-color: var(--color-gov);
1587
1643
  }
@@ -1717,6 +1773,18 @@
1717
1773
  background-color: color-mix(in oklab, var(--color-rose-400) 15%, transparent);
1718
1774
  }
1719
1775
  }
1776
+ .bg-sky-500\/10 {
1777
+ background-color: color-mix(in srgb, oklch(68.5% 0.169 237.323) 10%, transparent);
1778
+ @supports (color: color-mix(in lab, red, red)) {
1779
+ background-color: color-mix(in oklab, var(--color-sky-500) 10%, transparent);
1780
+ }
1781
+ }
1782
+ .bg-sky-500\/15 {
1783
+ background-color: color-mix(in srgb, oklch(68.5% 0.169 237.323) 15%, transparent);
1784
+ @supports (color: color-mix(in lab, red, red)) {
1785
+ background-color: color-mix(in oklab, var(--color-sky-500) 15%, transparent);
1786
+ }
1787
+ }
1720
1788
  .bg-surface {
1721
1789
  background-color: var(--color-surface);
1722
1790
  }
@@ -2231,6 +2299,9 @@
2231
2299
  .text-destructive {
2232
2300
  color: var(--color-destructive);
2233
2301
  }
2302
+ .text-emerald-700 {
2303
+ color: var(--color-emerald-700);
2304
+ }
2234
2305
  .text-foreground {
2235
2306
  color: var(--color-foreground);
2236
2307
  }
@@ -2288,6 +2359,9 @@
2288
2359
  .text-red-700 {
2289
2360
  color: var(--color-red-700);
2290
2361
  }
2362
+ .text-sky-700 {
2363
+ color: var(--color-sky-700);
2364
+ }
2291
2365
  .text-teal-600 {
2292
2366
  color: var(--color-teal-600);
2293
2367
  }
@@ -2917,6 +2991,9 @@
2917
2991
  .sm\:grid-cols-3 {
2918
2992
  grid-template-columns: repeat(3, minmax(0, 1fr));
2919
2993
  }
2994
+ .sm\:grid-cols-4 {
2995
+ grid-template-columns: repeat(4, minmax(0, 1fr));
2996
+ }
2920
2997
  .sm\:flex-row {
2921
2998
  flex-direction: row;
2922
2999
  }
@@ -2962,6 +3039,9 @@
2962
3039
  }
2963
3040
  }
2964
3041
  @media (width >= 64rem) {
3042
+ .lg\:col-span-2 {
3043
+ grid-column: span 2 / span 2;
3044
+ }
2965
3045
  .lg\:ml-\[256px\] {
2966
3046
  margin-left: 256px;
2967
3047
  }
@@ -2971,9 +3051,15 @@
2971
3051
  .lg\:hidden {
2972
3052
  display: none;
2973
3053
  }
3054
+ .lg\:grid-cols-2 {
3055
+ grid-template-columns: repeat(2, minmax(0, 1fr));
3056
+ }
2974
3057
  .lg\:grid-cols-4 {
2975
3058
  grid-template-columns: repeat(4, minmax(0, 1fr));
2976
3059
  }
3060
+ .lg\:grid-cols-\[280px_1fr\] {
3061
+ grid-template-columns: 280px 1fr;
3062
+ }
2977
3063
  .lg\:pt-0 {
2978
3064
  padding-top: 0px;
2979
3065
  }
@@ -2981,12 +3067,18 @@
2981
3067
  .dark\:text-amber-400:is([data-theme="dark"] *) {
2982
3068
  color: var(--color-amber-400);
2983
3069
  }
3070
+ .dark\:text-emerald-400:is([data-theme="dark"] *) {
3071
+ color: var(--color-emerald-400);
3072
+ }
2984
3073
  .dark\:text-green-400:is([data-theme="dark"] *) {
2985
3074
  color: var(--color-green-400);
2986
3075
  }
2987
3076
  .dark\:text-red-400:is([data-theme="dark"] *) {
2988
3077
  color: var(--color-red-400);
2989
3078
  }
3079
+ .dark\:text-sky-400:is([data-theme="dark"] *) {
3080
+ color: var(--color-sky-400);
3081
+ }
2990
3082
  .\[\&\>span\]\:line-clamp-1 > span {
2991
3083
  overflow: hidden;
2992
3084
  display: -webkit-box;