@haaaiawd/second-nature 0.1.51 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/openclaw.plugin.json +29 -29
  2. package/package.json +55 -55
  3. package/runtime/cli/commands/index.js +326 -325
  4. package/runtime/cli/ops/heartbeat-surface.d.ts +84 -84
  5. package/runtime/cli/ops/heartbeat-surface.js +100 -100
  6. package/runtime/cli/ops/ops-router.js +1555 -1482
  7. package/runtime/cli/ops/workspace-heartbeat-runner.d.ts +85 -85
  8. package/runtime/cli/ops/workspace-heartbeat-runner.js +242 -242
  9. package/runtime/connectors/base/contract.d.ts +111 -111
  10. package/runtime/connectors/base/failure-taxonomy.d.ts +13 -13
  11. package/runtime/connectors/base/failure-taxonomy.js +186 -186
  12. package/runtime/connectors/base/map-life-evidence.js +137 -137
  13. package/runtime/connectors/base/policy-layer.js +202 -202
  14. package/runtime/connectors/evidence-normalizer.d.ts +45 -0
  15. package/runtime/connectors/evidence-normalizer.js +115 -0
  16. package/runtime/connectors/manifest/manifest-schema.d.ts +152 -152
  17. package/runtime/connectors/manifest/manifest-schema.js +54 -54
  18. package/runtime/connectors/services/connector-executor-adapter.d.ts +20 -20
  19. package/runtime/connectors/services/connector-executor-adapter.js +645 -645
  20. package/runtime/core/second-nature/action/action-closure-recorder.d.ts +70 -0
  21. package/runtime/core/second-nature/action/action-closure-recorder.js +184 -0
  22. package/runtime/core/second-nature/action/action-proposal-builder.d.ts +70 -0
  23. package/runtime/core/second-nature/action/action-proposal-builder.js +217 -0
  24. package/runtime/core/second-nature/action/autonomy-policy-evaluator.d.ts +43 -0
  25. package/runtime/core/second-nature/action/autonomy-policy-evaluator.js +213 -0
  26. package/runtime/core/second-nature/action/policy-bound-dispatch.d.ts +69 -0
  27. package/runtime/core/second-nature/action/policy-bound-dispatch.js +112 -0
  28. package/runtime/core/second-nature/body/tool-affordance/affordance-side-effect.d.ts +49 -0
  29. package/runtime/core/second-nature/body/tool-affordance/affordance-side-effect.js +100 -0
  30. package/runtime/core/second-nature/control-plane/accepted-projection-loader.d.ts +45 -0
  31. package/runtime/core/second-nature/control-plane/accepted-projection-loader.js +85 -0
  32. package/runtime/core/second-nature/control-plane/heartbeat-orchestrator.d.ts +38 -0
  33. package/runtime/core/second-nature/control-plane/heartbeat-orchestrator.js +165 -0
  34. package/runtime/core/second-nature/guidance/guidance-proposal-consumer.d.ts +51 -0
  35. package/runtime/core/second-nature/guidance/guidance-proposal-consumer.js +113 -0
  36. package/runtime/core/second-nature/heartbeat/goal-lifecycle-policy.d.ts +24 -24
  37. package/runtime/core/second-nature/heartbeat/goal-lifecycle-policy.js +61 -61
  38. package/runtime/core/second-nature/heartbeat/heartbeat-loop.d.ts +97 -97
  39. package/runtime/core/second-nature/heartbeat/heartbeat-loop.js +397 -397
  40. package/runtime/core/second-nature/orchestrator/platform-capability-router.js +149 -149
  41. package/runtime/core/second-nature/perception/judgment-engine.d.ts +53 -0
  42. package/runtime/core/second-nature/perception/judgment-engine.js +239 -0
  43. package/runtime/core/second-nature/perception/perception-builder.d.ts +62 -0
  44. package/runtime/core/second-nature/perception/perception-builder.js +208 -0
  45. package/runtime/core/second-nature/perception/sensitivity-classifier.d.ts +37 -0
  46. package/runtime/core/second-nature/perception/sensitivity-classifier.js +87 -0
  47. package/runtime/core/second-nature/quiet-dream/dream-consolidation-runner.d.ts +44 -0
  48. package/runtime/core/second-nature/quiet-dream/dream-consolidation-runner.js +180 -0
  49. package/runtime/core/second-nature/quiet-dream/dream-scheduler.d.ts +36 -0
  50. package/runtime/core/second-nature/quiet-dream/dream-scheduler.js +105 -0
  51. package/runtime/core/second-nature/quiet-dream/memory-projection-lifecycle.d.ts +36 -0
  52. package/runtime/core/second-nature/quiet-dream/memory-projection-lifecycle.js +151 -0
  53. package/runtime/core/second-nature/quiet-dream/quiet-daily-review-builder.d.ts +46 -0
  54. package/runtime/core/second-nature/quiet-dream/quiet-daily-review-builder.js +123 -0
  55. package/runtime/observability/causal-loop-health.d.ts +44 -0
  56. package/runtime/observability/causal-loop-health.js +118 -0
  57. package/runtime/observability/diagnostic-redaction.d.ts +43 -0
  58. package/runtime/observability/diagnostic-redaction.js +114 -0
  59. package/runtime/observability/loop-stage-event-sink.d.ts +43 -0
  60. package/runtime/observability/loop-stage-event-sink.js +148 -0
  61. package/runtime/observability/loop-status.d.ts +46 -0
  62. package/runtime/observability/loop-status.js +85 -0
  63. package/runtime/shared/types/index.js +3 -0
  64. package/runtime/shared/types/v8-contracts.d.ts +86 -0
  65. package/runtime/shared/types/v8-contracts.js +84 -0
  66. package/runtime/storage/db/schema/index.d.ts +1 -0
  67. package/runtime/storage/db/schema/index.js +1 -0
  68. package/runtime/storage/db/schema/v8-entities.d.ts +1973 -0
  69. package/runtime/storage/db/schema/v8-entities.js +160 -0
  70. package/runtime/storage/v8-state-stores.d.ts +147 -0
  71. package/runtime/storage/v8-state-stores.js +491 -0
@@ -1,1482 +1,1555 @@
1
- /**
2
- * Shared ops command dispatch for CLI + tool surfaces (T1.1.3, T1.2.2).
3
- *
4
- * v7 additions (T-ROS.C.1): self_health, tool_affordance, connector_test --wet,
5
- * heartbeat_digest, narrative:diff, timeline, restore, runtime_secret_bootstrap.
6
- * All commands return RuntimeOpsEnvelope.
7
- */
8
- import { createHash } from "node:crypto";
9
- import fs from "node:fs";
10
- import path from "node:path";
11
- import { heartbeatCheck, } from "./heartbeat-surface.js";
12
- import { showOperatorFallback, OperatorFallbackNotFoundError, } from "./show-operator-fallback.js";
13
- import { probeHostCapability } from "../host-capability/probe-host-capability.js";
14
- import { recordHostCapability } from "../host-capability/record-host-capability.js";
15
- import { runNearRealConnectorSmoke } from "../../connectors/near-real/near-real-connector-smoke.js";
16
- import { scanConnectorManifests } from "../../connectors/registry/manifest-scanner.js";
17
- import { parseConnectorManifestV6 } from "../../connectors/manifest/manifest-parser.js";
18
- import { connectorInit } from "../commands/connector-init.js";
19
- import { connectorBehaviorAdd } from "../commands/connector-behavior.js";
20
- import { connectorStatus, connectorTest } from "../commands/connector-status.js";
21
- import { goalCommand } from "../commands/goal.js";
22
- // v7 observability services (T-ROS.C.1)
23
- import { getSelfHealthSnapshot, ensureMinimumProbes, } from "../../observability/services/self-health-snapshot.js";
24
- import { generateHeartbeatDigest, } from "../../observability/services/heartbeat-digest-assembler.js";
25
- import { queryNarrativeTimeline, queryNarrativeDiff, NarrativeVersionNotFoundError, } from "../../observability/services/narrative-timeline-query-service.js";
26
- import { viewSecretAnchor, } from "../../observability/services/runtime-secret-anchor-view.js";
27
- import { writeRestoreAudit, } from "../../observability/services/restore-audit-service.js";
28
- import { createHistoryDigestStore } from "../../storage/services/history-digest-store.js";
29
- // T-ROS.C.3: ManualRunDispatcher and its deps
30
- import { createManualRunDispatcher, } from "./manual-run-dispatcher.js";
31
- import { createExperienceWriter } from "../../core/second-nature/body/tool-experience/experience-writer.js";
32
- import { createCapabilityProbeResultStore, createToolExperienceStore, } from "../../storage/services/tool-experience-store.js";
33
- import { createWetProbeRunner } from "../../connectors/base/wet-probe-runner.js";
34
- import { CapabilityContractRegistryV7 } from "../../connectors/base/manifest-v7.js";
35
- // v7 T-V7C.C.6: Dream scheduling deps for heartbeat_check quiet→dream auto-trigger
36
- import { scheduleDream } from "../../dream/dream-scheduler.js";
37
- import { createDreamInputLoader } from "../../dream/dream-input-loader.js";
38
- import { createDiaryDreamStore } from "../../storage/services/diary-dream-store.js";
39
- // v7 T-CP.C.3 / T-BTS.C.5: heartbeat loop policies and breaker
40
- import { createGoalLifecyclePolicy } from "../../core/second-nature/heartbeat/goal-lifecycle-policy.js";
41
- import { createIdleCuriosityPolicy } from "../../core/second-nature/heartbeat/idle-curiosity-policy.js";
42
- import { createCircuitBreakerManager } from "../../core/second-nature/body/circuit-breaker/circuit-breaker-manager.js";
43
- import { createProbeSignalAdapter } from "../../core/second-nature/body/probe-signal-adapter.js";
44
- function coerceProbeOnlyFlag(input) {
45
- const v = input?.probeOnly;
46
- return v === true || v === "true" || v === 1 || v === "1";
47
- }
48
- /**
49
- * v7 T-V7C.C.6: Build a minimal QuietDreamSchedulePort backed by the state DB.
50
- * When a source-backed Quiet write completes, this port triggers Dream scheduling
51
- * via the standard scheduleDream path (rules-only mode when no model port).
52
- */
53
- function createQuietDreamSchedulePort(state) {
54
- return {
55
- async scheduleDream({ triggerKind, runId, traceId }) {
56
- const dreamStore = createDiaryDreamStore(state);
57
- const inputLoader = createDreamInputLoader({ database: state });
58
- const statePort = {
59
- async loadDreamInputs(query) {
60
- return inputLoader.loadDreamInputs(query);
61
- },
62
- async writeDreamOutput(output) {
63
- // Bridge: dream-engine emits dream/types DreamOutput; diary-dream-store expects shared/types.
64
- // Structures are identical at runtime; TS strictness requires the cast.
65
- await dreamStore.appendDreamOutput(output);
66
- return { outputId: output.outputId, status: "acknowledged" };
67
- },
68
- async markDreamOutputLifecycle(input) {
69
- // transitionDreamOutputLifecycle only accepts accepted|archived.
70
- if (input.newStatus !== "accepted" && input.newStatus !== "archived") {
71
- return { outputId: input.outputId, status: "degraded" };
72
- }
73
- await dreamStore.transitionDreamOutputLifecycle(input.outputId, input.newStatus);
74
- return { outputId: input.outputId, status: "acknowledged" };
75
- },
76
- };
77
- const result = await scheduleDream({
78
- triggerKind,
79
- runId,
80
- traceId,
81
- statePort,
82
- windowKey: "quiet_completion",
83
- });
84
- return { status: result.status, reason: result.reason };
85
- },
86
- };
87
- }
88
- const SNAPSHOT_TABLE_BY_KIND = {
89
- identity_profile: "identity_profile",
90
- agent_goal: "agent_goal",
91
- tool_experience: "tool_experience",
92
- daily_diary: "daily_diary_index",
93
- dream_output: "dream_output_index",
94
- narrative_timeline: "narrative_timeline",
95
- };
96
- const DEFAULT_SNAPSHOT_KINDS = [
97
- "identity_profile",
98
- "agent_goal",
99
- "tool_experience",
100
- "daily_diary",
101
- "dream_output",
102
- "narrative_timeline",
103
- ];
104
- function coerceRestorableKinds(value) {
105
- if (!Array.isArray(value))
106
- return undefined;
107
- const valid = new Set(DEFAULT_SNAPSHOT_KINDS);
108
- return value.filter((item) => typeof item === "string" && valid.has(item));
109
- }
110
- function tableExists(state, table) {
111
- const result = state.sqlite.exec(`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ? LIMIT 1`, [table]);
112
- return result.length > 0 && result[0].values.length > 0;
113
- }
114
- function readRowsFromTable(state, table) {
115
- const result = state.sqlite.exec(`SELECT * FROM ${table}`);
116
- if (result.length === 0 || result[0].values.length === 0)
117
- return [];
118
- const columns = result[0].columns;
119
- return result[0].values.map((row) => {
120
- const out = {};
121
- columns.forEach((column, index) => {
122
- out[column] = row[index];
123
- });
124
- return out;
125
- });
126
- }
127
- function stringArray(value) {
128
- return Array.isArray(value)
129
- ? value.filter((item) => typeof item === "string")
130
- : [];
131
- }
132
- function textInput(input, key) {
133
- const value = input?.[key];
134
- if (typeof value !== "string")
135
- return undefined;
136
- const trimmed = value.trim();
137
- return trimmed.length > 0 ? trimmed : undefined;
138
- }
139
- function buildSnapshotNarrativeDelta(input, snapshotId, rowCounts) {
140
- const explicit = input?.narrativeSnapshot &&
141
- typeof input.narrativeSnapshot === "object" &&
142
- !Array.isArray(input.narrativeSnapshot)
143
- ? input.narrativeSnapshot
144
- : {};
145
- const from = (key) => input?.[key] ?? explicit[key];
146
- const sourceRefs = stringArray(from("sourceRefs"));
147
- return {
148
- focus: from("focus") ?? "workspace_state",
149
- progress: from("progress") ??
150
- `snapshot_captured:${Object.entries(rowCounts)
151
- .map(([kind, count]) => `${kind}=${count}`)
152
- .join(",")}`,
153
- nextIntent: from("nextIntent") ?? "restore_ready",
154
- toneSignal: from("toneSignal") ?? "system_maintenance",
155
- acceptedGoalId: from("acceptedGoalId") ?? undefined,
156
- sourceRefs: sourceRefs.length > 0
157
- ? sourceRefs
158
- : [`restore_snapshot:${snapshotId}`, "runtime_ops:snapshot_capture"],
159
- reasonCode: from("reasonCode") ?? "snapshot_captured",
160
- summaryText: from("summaryText") ?? `Captured restore snapshot ${snapshotId}`,
161
- };
162
- }
163
- function hashNarrativeSnapshot(input) {
164
- return createHash("sha256")
165
- .update(JSON.stringify({
166
- previousHash: input.previousHash,
167
- snapshotId: input.snapshotId,
168
- delta: input.delta,
169
- createdAt: input.createdAt,
170
- }))
171
- .digest("hex");
172
- }
173
- function resolveManifestPath(manifestPath, workspaceRoot) {
174
- if (path.isAbsolute(manifestPath))
175
- return manifestPath;
176
- return path.join(workspaceRoot ?? process.cwd(), manifestPath);
177
- }
178
- function registerConnectorForWetProbe(input) {
179
- if (input.entry.manifestPath) {
180
- try {
181
- const manifestText = fs.readFileSync(resolveManifestPath(input.entry.manifestPath, input.workspaceRoot), "utf-8");
182
- const parsed = JSON.parse(manifestText);
183
- const registered = input.registryV7.register(parsed);
184
- if (registered.ok && input.registryV7.hasCapability(input.entry.platformId, input.selectedCapabilityId)) {
185
- return;
186
- }
187
- }
188
- catch {
189
- // Non-v7 or YAML workspace manifests are projected below.
190
- }
191
- }
192
- input.registryV7.register({
193
- platformId: input.entry.platformId,
194
- capabilities: input.entry.capabilities.map((capabilityId) => ({
195
- capabilityId,
196
- intent: capabilityId,
197
- probeConfig: capabilityId === input.selectedCapabilityId && input.safeEndpoint
198
- ? {
199
- safeEndpoint: input.safeEndpoint,
200
- idempotencyClass: "read_only",
201
- }
202
- : undefined,
203
- })),
204
- channelPriority: ["runtime_ops"],
205
- credentialTypes: ["runtime_ops_probe"],
206
- });
207
- }
208
- async function captureRuntimeSnapshot(deps, input) {
209
- const generatedAt = new Date().toISOString();
210
- if (!deps.state || !deps.restoreSnapshotStore) {
211
- return {
212
- ok: false,
213
- command: "snapshot:capture",
214
- runtimeMode: "unavailable",
215
- surfaceMode: "cli",
216
- generatedAt,
217
- error: {
218
- code: "SNAPSHOT_CAPTURE_DEPS_UNAVAILABLE",
219
- message: "snapshot:capture requires state DB and RestoreSnapshotStore in OpsRouterDeps",
220
- nextStep: "wire_state_and_restore_snapshot_store_into_ops_router",
221
- },
222
- warnings: [],
223
- sourceRefs: [],
224
- };
225
- }
226
- const snapshotId = textInput(input, "snapshotId") ??
227
- `snapshot:${Date.now()}:${Math.random().toString(36).slice(2, 8)}`;
228
- const requestedKinds = coerceRestorableKinds(input?.entityWhitelist) ?? [...DEFAULT_SNAPSHOT_KINDS];
229
- const rowCounts = {};
230
- const warnings = [];
231
- for (const kind of requestedKinds) {
232
- const table = SNAPSHOT_TABLE_BY_KIND[kind];
233
- if (!tableExists(deps.state, table)) {
234
- rowCounts[kind] = 0;
235
- warnings.push(`table_missing:${kind}:${table}`);
236
- continue;
237
- }
238
- rowCounts[kind] = readRowsFromTable(deps.state, table).length;
239
- }
240
- const historyStore = createHistoryDigestStore(deps.state);
241
- const previousHash = (await historyStore.listNarrativeTimeline({ limit: 1 }))[0]?.currentHash ?? "";
242
- const delta = buildSnapshotNarrativeDelta(input, snapshotId, rowCounts);
243
- const currentHash = hashNarrativeSnapshot({
244
- previousHash,
245
- snapshotId,
246
- delta,
247
- createdAt: generatedAt,
248
- });
249
- await historyStore.appendNarrativeTimeline({
250
- timelineId: snapshotId,
251
- entryType: "owner.override",
252
- subjectId: textInput(input, "subjectId") ?? snapshotId,
253
- delta,
254
- previousHash,
255
- currentHash,
256
- createdAt: generatedAt,
257
- });
258
- const payload = {};
259
- const capturedKinds = [];
260
- for (const kind of requestedKinds) {
261
- const table = SNAPSHOT_TABLE_BY_KIND[kind];
262
- if (!tableExists(deps.state, table))
263
- continue;
264
- const rows = readRowsFromTable(deps.state, table);
265
- rowCounts[kind] = rows.length;
266
- if (rows.length > 0) {
267
- payload[kind] = rows;
268
- capturedKinds.push(kind);
269
- }
270
- }
271
- const snapshot = await deps.restoreSnapshotStore.captureSnapshot({
272
- snapshotId,
273
- entityWhitelist: requestedKinds,
274
- payload,
275
- capturedAt: generatedAt,
276
- });
277
- return {
278
- ok: true,
279
- command: "snapshot:capture",
280
- runtimeMode: "workspace_full_runtime",
281
- surfaceMode: "cli",
282
- generatedAt,
283
- data: {
284
- snapshotId: snapshot.snapshotId,
285
- capturedAt: snapshot.capturedAt,
286
- entityWhitelist: snapshot.entityWhitelist,
287
- capturedKinds,
288
- rowCounts,
289
- narrativeVersion: snapshotId,
290
- },
291
- warnings,
292
- sourceRefs: [
293
- "storage/services/restore-snapshot-store.ts",
294
- "storage/services/history-digest-store.ts",
295
- ],
296
- };
297
- }
298
- /**
299
- * T1.2.8 — static local adapter: all checks return `unknown` when no real host is available.
300
- * Allows `capability_probe` to be called from CLI / workspace bridge without requiring a live host.
301
- */
302
- function createStaticUnknownAdapter(workspaceRoot) {
303
- const now = new Date().toISOString();
304
- const unknownResult = (name) => ({
305
- name,
306
- verdict: "unknown",
307
- observedAt: now,
308
- reason: "static_local_probe_no_host_context",
309
- evidenceRefs: [],
310
- });
311
- function checkDeliveryTarget() {
312
- if (!workspaceRoot) {
313
- return { status: "target_none", evidenceRefs: [], reason: "no_workspace_root_provided" };
314
- }
315
- const deliveryCapabilities = ["message.send", "comment.reply"];
316
- const scanned = scanConnectorManifests(workspaceRoot);
317
- for (const manifestFile of scanned) {
318
- const parsed = parseConnectorManifestV6(manifestFile.content, manifestFile.path);
319
- if (parsed.ok && parsed.manifest.capabilities.some((cap) => deliveryCapabilities.includes(cap.id))) {
320
- return {
321
- status: "target_available",
322
- evidenceRefs: [
323
- {
324
- id: `delivery:${parsed.manifest.platformId}`,
325
- kind: "workspace_artifact",
326
- uri: `workspace://connectors/${parsed.manifest.platformId}/manifest.yaml`,
327
- observedAt: now,
328
- },
329
- ],
330
- };
331
- }
332
- }
333
- return { status: "target_none", evidenceRefs: [], reason: "no_delivery_connector_found_in_workspace" };
334
- }
335
- return {
336
- checkPluginLoad: () => unknownResult("plugin_load"),
337
- checkHeartbeatBridge: () => unknownResult("heartbeat_bridge"),
338
- checkHeartbeatToolInvocation: () => unknownResult("heartbeat_tool_invocation"),
339
- checkDeliveryTarget,
340
- checkAckDropBehavior: () => unknownResult("ack_drop"),
341
- checkHookSupport: () => [],
342
- };
343
- }
344
- export function createOpsRouter(deps) {
345
- return {
346
- heartbeatCheck: (input) => heartbeatCheck({
347
- ...input,
348
- runtimeAvailable: input.runtimeAvailable ?? deps.runtimeAvailable,
349
- readModels: input.readModels ?? deps.readModels,
350
- runtimeRecorder: input.runtimeRecorder ?? deps.runtimeRecorder,
351
- state: input.state ?? deps.state,
352
- workspaceRoot: input.workspaceRoot ?? deps.workspaceRoot,
353
- connectorExecutor: input.connectorExecutor ?? deps.connectorExecutor,
354
- connectorRegistry: input
355
- ?.connectorRegistry ?? deps.connectorRegistry,
356
- digestOpts: input.digestOpts,
357
- dreamSchedulePort: input.dreamSchedulePort,
358
- }),
359
- async dispatch(command, input) {
360
- if (command === "heartbeat_check") {
361
- const runtimeAvailable = typeof input?.runtimeAvailable === "boolean"
362
- ? input.runtimeAvailable
363
- : deps.runtimeAvailable;
364
- // v7 T-V7C.C.2: assemble affordance map and experience writer for breaker-aware heartbeat.
365
- let affordanceMap;
366
- if (deps.toolAffordancePort) {
367
- try {
368
- affordanceMap = await deps.toolAffordancePort.assembleAffordanceMap({});
369
- }
370
- catch {
371
- // degrade gracefully; guard-layer will skip breaker check without affordanceMap
372
- }
373
- }
374
- let experienceWriter;
375
- if (deps.state) {
376
- experienceWriter = createExperienceWriter(createToolExperienceStore(deps.state));
377
- }
378
- // v7 T-V7C.C.6: assemble digest opts when auditStore is wired.
379
- let digestOpts;
380
- if (deps.auditStore) {
381
- digestOpts = {
382
- assemblerDeps: {
383
- auditStore: deps.auditStore,
384
- ...deps.heartbeatDigestDeps,
385
- },
386
- };
387
- }
388
- // v7 T-V7C.C.6: assemble dream schedule port when state DB is wired.
389
- let dreamSchedulePort;
390
- if (deps.state) {
391
- dreamSchedulePort = createQuietDreamSchedulePort(deps.state);
392
- }
393
- // v7 T-CP.C.3: assemble goal lifecycle and idle curiosity policies.
394
- const goalLifecyclePolicy = createGoalLifecyclePolicy();
395
- const idleCuriosityPolicy = createIdleCuriosityPolicy();
396
- // v7 T-BTS.C.5: assemble circuit breaker manager when state DB is wired.
397
- let circuitBreakerManager;
398
- if (deps.state) {
399
- const probeResultStore = createCapabilityProbeResultStore(deps.state);
400
- const toolExpStore = createToolExperienceStore(deps.state);
401
- const probeAdapter = createProbeSignalAdapter({
402
- wetProbeRunner: createWetProbeRunner(),
403
- probeResultStore,
404
- toolExperienceStore: toolExpStore,
405
- });
406
- const registryV7 = new CapabilityContractRegistryV7();
407
- circuitBreakerManager = createCircuitBreakerManager({
408
- database: deps.state,
409
- probeAdapter,
410
- registry: registryV7,
411
- });
412
- }
413
- try {
414
- const result = await heartbeatCheck({
415
- probeOnly: coerceProbeOnlyFlag(input),
416
- runtimeAvailable,
417
- fakeControlPlanePassthrough: input?.fakeControlPlanePassthrough &&
418
- typeof input.fakeControlPlanePassthrough === "object"
419
- ? input.fakeControlPlanePassthrough
420
- : undefined,
421
- readModels: input?.readModels ??
422
- deps.readModels,
423
- runtimeRecorder: input
424
- ?.runtimeRecorder ?? deps.runtimeRecorder,
425
- state: input?.state ??
426
- deps.state,
427
- workspaceRoot: input
428
- ?.workspaceRoot ?? deps.workspaceRoot,
429
- timestamp: typeof input?.timestamp === "string" ? input.timestamp : undefined,
430
- sessionContext: typeof input?.sessionContext === "string"
431
- ? input.sessionContext
432
- : undefined,
433
- scopeHint: input?.scopeHint,
434
- connectorExecutor: input
435
- ?.connectorExecutor ?? deps.connectorExecutor,
436
- connectorRegistry: input
437
- ?.connectorRegistry ?? deps.connectorRegistry,
438
- affordanceMap,
439
- experienceWriter,
440
- digestOpts,
441
- dreamSchedulePort,
442
- goalLifecyclePolicy,
443
- idleCuriosityPolicy,
444
- circuitBreakerManager,
445
- });
446
- if (result.ok &&
447
- result.surfaceMode === "workspace_full_runtime" &&
448
- !coerceProbeOnlyFlag(input) &&
449
- deps.state &&
450
- deps.restoreSnapshotStore) {
451
- try {
452
- const capture = await captureRuntimeSnapshot(deps, {
453
- snapshotId: `heartbeat:${result.decisionId ?? "cycle"}:${Date.now()}`,
454
- subjectId: result.decisionId ?? "heartbeat_check",
455
- reasonCode: "heartbeat_check",
456
- summaryText: `Heartbeat ${result.status} captured bounded restore snapshot`,
457
- focus: result.status,
458
- progress: result.reasons.join(",") || "heartbeat_completed",
459
- nextIntent: "continue_runtime_loop",
460
- sourceRefs: result.decisionId
461
- ? [`heartbeat:${result.decisionId}`]
462
- : ["heartbeat:runtime"],
463
- });
464
- if (capture.ok) {
465
- result.reasons = [...result.reasons, "restore_snapshot_captured"];
466
- }
467
- }
468
- catch (err) {
469
- const msg = err instanceof Error ? err.message : String(err);
470
- result.reasons = [...result.reasons, `restore_snapshot_capture_failed:${msg}`];
471
- }
472
- }
473
- return result;
474
- }
475
- catch (err) {
476
- const msg = err instanceof Error ? err.message : String(err);
477
- const envelope = {
478
- ok: false,
479
- command: "heartbeat_check",
480
- runtimeMode: runtimeAvailable ? "workspace_full_runtime" : "unavailable",
481
- surfaceMode: "cli",
482
- generatedAt: new Date().toISOString(),
483
- error: {
484
- code: "HEARTBEAT_CYCLE_EXCEPTION",
485
- message: `heartbeat_check cycle threw unexpectedly: ${msg.slice(0, 200)}`,
486
- nextStep: "check_logs_and_report",
487
- },
488
- warnings: [],
489
- sourceRefs: [],
490
- };
491
- return envelope;
492
- }
493
- }
494
- if (command === "fallback") {
495
- const ref = typeof input?.ref === "string" ? input.ref.trim() : "";
496
- if (!ref) {
497
- return {
498
- ok: false,
499
- error: {
500
- code: "MISSING_FALLBACK_REF",
501
- message: "fallback requires args.ref (e.g. fallback:…)",
502
- requiredUserInput: ["ref"],
503
- nextStep: "reinvoke_with_ref",
504
- },
505
- };
506
- }
507
- if (!deps.readModels?.loadFallbackView) {
508
- return {
509
- ok: false,
510
- error: {
511
- code: "FALLBACK_READ_MODEL_UNAVAILABLE",
512
- message: "Operator fallback view requires workspace read models",
513
- requiredUserInput: ["ref"],
514
- nextStep: "wire_read_models_into_ops_router",
515
- },
516
- };
517
- }
518
- return (async () => {
519
- try {
520
- const data = await showOperatorFallback(ref, deps.readModels);
521
- return { ok: true, command: "fallback", data };
522
- }
523
- catch (error) {
524
- if (error instanceof OperatorFallbackNotFoundError) {
525
- return {
526
- ok: false,
527
- command: "fallback",
528
- error: {
529
- code: error.code,
530
- message: error.message,
531
- requiredUserInput: ["ref"],
532
- nextStep: "verify_fallback_ref_from_delivery_audit",
533
- },
534
- };
535
- }
536
- throw error;
537
- }
538
- })();
539
- }
540
- if (command === "capability_probe") {
541
- // T1.2.8 (SN-CODE-03): run host capability probe with static unknown adapter (CLI context).
542
- // Persists report when observabilityDb is available; returns safe JSON subset.
543
- return (async () => {
544
- const adapter = createStaticUnknownAdapter(deps.workspaceRoot);
545
- const docCheckedAt = new Date().toISOString();
546
- const report = probeHostCapability({
547
- adapter,
548
- docLinks: [],
549
- docCheckedAt,
550
- });
551
- if (deps.observabilityDb) {
552
- await recordHostCapability(deps.observabilityDb, report);
553
- }
554
- return {
555
- ok: true,
556
- command: "capability_probe",
557
- data: {
558
- reportId: report.reportId,
559
- generatedAt: report.generatedAt,
560
- deliveryTarget: report.deliveryTarget,
561
- pluginLoad: { verdict: report.pluginLoad.verdict },
562
- heartbeatBridge: { verdict: report.heartbeatBridge.verdict },
563
- heartbeatToolInvocation: {
564
- verdict: report.heartbeatToolInvocation.verdict,
565
- },
566
- ackDropBehavior: { verdict: report.ackDropBehavior.verdict },
567
- conflictCount: report.conflictRecords.length,
568
- recommendedNextStep: report.recommendedNextStep,
569
- note: "static_local_probe: all verdicts are unknown without live host context",
570
- },
571
- };
572
- })();
573
- }
574
- if (command === "near_real_smoke") {
575
- // T3.3.2 (SN-CODE-05): wrap runNearRealConnectorSmoke as an ops surface command.
576
- // Requires state + observabilityDb + workspaceRoot to be wired into OpsRouterDeps.
577
- if (!deps.state || !deps.observabilityDb || !deps.workspaceRoot) {
578
- return {
579
- ok: false,
580
- command: "near_real_smoke",
581
- error: {
582
- code: "NEAR_REAL_SMOKE_DEPS_UNAVAILABLE",
583
- message: "near_real_smoke requires state, observabilityDb, and workspaceRoot in OpsRouterDeps",
584
- nextStep: "wire_deps_into_ops_router",
585
- },
586
- };
587
- }
588
- return (async () => {
589
- const result = await runNearRealConnectorSmoke({
590
- state: deps.state,
591
- observabilityDb: deps.observabilityDb,
592
- workspaceRoot: deps.workspaceRoot,
593
- });
594
- return {
595
- ok: true,
596
- command: "near_real_smoke",
597
- data: result,
598
- };
599
- })();
600
- }
601
- if (command === "connector_init") {
602
- // T1.3.1 (SN-CODE-06): generate connector manifest stub.
603
- return (async () => {
604
- const result = await connectorInit({
605
- platformId: typeof input?.platformId === "string" ? input.platformId : "",
606
- family: typeof input?.family === "string"
607
- ? input.family
608
- : undefined,
609
- displayName: typeof input?.displayName === "string" ? input.displayName : undefined,
610
- runnerKind: typeof input?.runnerKind === "string"
611
- ? input.runnerKind
612
- : undefined,
613
- force: Boolean(input?.force),
614
- workspaceRoot: deps.workspaceRoot,
615
- });
616
- return result;
617
- })();
618
- }
619
- if (command === "connector_behavior_add") {
620
- return connectorBehaviorAdd({
621
- platformId: typeof input?.platformId === "string" ? input.platformId : "",
622
- behaviorId: typeof input?.behaviorId === "string"
623
- ? input.behaviorId
624
- : typeof input?.capabilityId === "string"
625
- ? input.capabilityId
626
- : "",
627
- description: typeof input?.description === "string" ? input.description : undefined,
628
- channel: typeof input?.channel === "string" ? input.channel : undefined,
629
- sourceRefs: input?.sourceRefs,
630
- observedCount: typeof input?.observedCount === "number" ? input.observedCount : undefined,
631
- workspaceRoot: typeof input?.workspaceRoot === "string"
632
- ? input.workspaceRoot
633
- : deps.workspaceRoot,
634
- });
635
- }
636
- if (command === "connector_status") {
637
- return connectorStatus(deps.registry, undefined, {
638
- includeHealth: Boolean(input?.includeHealth),
639
- workspaceRoot: typeof input?.workspaceRoot === "string"
640
- ? input.workspaceRoot
641
- : deps.workspaceRoot,
642
- });
643
- }
644
- if (command === "connector_test") {
645
- // v7 T-V7C.C.1: dryRun=false is the canonical wet probe switch.
646
- const isWet = input?.wet === true ||
647
- input?.wet === "true" ||
648
- input?.dryRun === false ||
649
- input?.dryRun === "false";
650
- const result = await connectorTest(deps.registry, {
651
- platformId: typeof input?.platformId === "string" ? input.platformId : "",
652
- dryRun: isWet ? false : (input?.dryRun === false ? false : true),
653
- workspaceRoot: typeof input?.workspaceRoot === "string"
654
- ? input.workspaceRoot
655
- : deps.workspaceRoot,
656
- });
657
- if (!isWet || !result.ok) {
658
- return result;
659
- }
660
- const data = result.data && typeof result.data === "object"
661
- ? result.data
662
- : {};
663
- const capabilities = Array.isArray(data.capabilities)
664
- ? data.capabilities.filter((item) => typeof item === "string")
665
- : [];
666
- const capabilityId = textInput(input, "capabilityId") ?? capabilities[0] ?? "";
667
- if (!capabilityId) {
668
- return {
669
- ok: false,
670
- command: "connector_test",
671
- error: {
672
- code: "MISSING_CAPABILITY_ID",
673
- message: "wet connector_test requires capabilityId or at least one connector capability",
674
- requiredUserInput: ["capabilityId"],
675
- nextStep: "reinvoke_with_capability_id",
676
- },
677
- };
678
- }
679
- const platformId = String(data.platformId ?? input?.platformId ?? "");
680
- const registryEntry = deps.registry?.describeConnector(platformId);
681
- if (!registryEntry) {
682
- return result;
683
- }
684
- const registryV7 = new CapabilityContractRegistryV7();
685
- registerConnectorForWetProbe({
686
- registryV7,
687
- entry: {
688
- platformId: registryEntry.platformId,
689
- capabilities: registryEntry.capabilities,
690
- manifestPath: registryEntry.manifestPath,
691
- },
692
- workspaceRoot: typeof input?.workspaceRoot === "string"
693
- ? input.workspaceRoot
694
- : deps.workspaceRoot,
695
- selectedCapabilityId: capabilityId,
696
- safeEndpoint: textInput(input, "safeEndpoint"),
697
- });
698
- const wetResult = await createWetProbeRunner().runWetProbe(platformId, capabilityId, registryV7);
699
- const warnings = [];
700
- let persistedProbeResult = false;
701
- if (deps.state) {
702
- await createCapabilityProbeResultStore(deps.state).appendProbeResult(wetResult.probeResult);
703
- persistedProbeResult = true;
704
- }
705
- else {
706
- warnings.push("state_db_unavailable:capability_probe_result_not_persisted");
707
- }
708
- return {
709
- // T-V7C.C.5: only "available" (HTTP 200-299) counts as success;
710
- // "degraded" (429/503) and "unavailable" both result in ok=false.
711
- ok: wetResult.probeResult.actualStatus === "available",
712
- command: "connector_test",
713
- data: {
714
- ...data,
715
- dryRun: false,
716
- capabilityId,
717
- actualStatus: wetResult.probeResult.actualStatus,
718
- httpStatus: wetResult.probeResult.httpStatus ?? wetResult.httpStatus,
719
- probeResultId: wetResult.probeResult.probeResultId,
720
- probeConfigRef: wetResult.probeResult.probeConfigRef,
721
- sampleResponseRef: wetResult.probeResult.sampleResponseRef,
722
- persistedProbeResult,
723
- triggerSource: "manual_run",
724
- affectsHeartbeatCadence: false,
725
- note: "wet probe mode: executed safe probe endpoint and persisted capability_probe_result when state DB is available",
726
- },
727
- warnings,
728
- };
729
- }
730
- if (command === "connector:run") {
731
- // T-ROS.C.3: manual connector execution — isolated from heartbeat cadence
732
- const platformId = typeof input?.platformId === "string" ? input.platformId : "";
733
- const capabilityId = typeof input?.capabilityId === "string" ? input.capabilityId : "";
734
- if (!platformId || !capabilityId) {
735
- return {
736
- ok: false,
737
- command: "connector:run",
738
- error: {
739
- code: "MISSING_PLATFORM_OR_CAPABILITY_ID",
740
- message: "connector:run requires platformId and capabilityId",
741
- requiredUserInput: ["platformId", "capabilityId"],
742
- nextStep: "reinvoke_with_platform_and_capability_id",
743
- },
744
- };
745
- }
746
- if (!deps.connectorExecutor || !deps.state) {
747
- return {
748
- ok: false,
749
- command: "connector:run",
750
- error: {
751
- code: "MANUAL_RUN_DEPS_UNAVAILABLE",
752
- message: "connector:run requires connectorExecutor and state database",
753
- nextStep: "wire_connector_executor_and_state_into_ops_router",
754
- },
755
- };
756
- }
757
- const toolExperienceStore = createToolExperienceStore(deps.state);
758
- const experienceWriter = createExperienceWriter(toolExperienceStore);
759
- const wetProbeRunner = createWetProbeRunner();
760
- const registryV7 = new CapabilityContractRegistryV7();
761
- // Populate V7 registry from dynamic registry if available (best-effort)
762
- if (deps.registry) {
763
- for (const entry of deps.registry.listConnectors()) {
764
- if (entry.manifestPath) {
765
- try {
766
- const manifestText = fs.readFileSync(entry.manifestPath, "utf-8");
767
- const manifest = JSON.parse(manifestText);
768
- registryV7.register(manifest);
769
- }
770
- catch {
771
- // Skip manifests that can't be read or don't validate as V7
772
- }
773
- }
774
- }
775
- }
776
- const dispatcher = createManualRunDispatcher({
777
- connectorExecutor: deps.connectorExecutor,
778
- experienceWriter,
779
- wetProbeRunner,
780
- registryV7,
781
- });
782
- return dispatcher.runConnector({
783
- platformId,
784
- capabilityId,
785
- payload: typeof input?.payload === "object" && input?.payload !== null
786
- ? input.payload
787
- : undefined,
788
- caller: typeof input?.caller === "string" ? input.caller : undefined,
789
- reason: typeof input?.reason === "string" ? input.reason : undefined,
790
- });
791
- }
792
- if (command === "goal") {
793
- const rawAction = typeof input?.action === "string" ? input.action : "list";
794
- const action = ["set", "list", "accept", "reject"].includes(rawAction)
795
- ? rawAction
796
- : "list";
797
- const sanitizeText = (v, maxLen = 1000) => {
798
- if (typeof v !== "string")
799
- return undefined;
800
- const trimmed = v.trim();
801
- if (trimmed.length === 0)
802
- return undefined;
803
- return trimmed.slice(0, maxLen);
804
- };
805
- return goalCommand(deps.state, {
806
- action,
807
- goalId: typeof input?.goalId === "string" ? input.goalId.trim().slice(0, 128) : undefined,
808
- description: sanitizeText(input?.description),
809
- completionCriteria: sanitizeText(input?.completionCriteria),
810
- // T1.4.2: criteria alias for completionCriteria
811
- criteria: sanitizeText(input?.criteria),
812
- risk: typeof input?.risk === "string"
813
- ? input.risk
814
- : undefined,
815
- kind: typeof input?.kind === "string"
816
- ? input.kind
817
- : undefined,
818
- statusFilter: typeof input?.statusFilter === "string" ? input.statusFilter : undefined,
819
- originFilter: typeof input?.originFilter === "string" ? input.originFilter : undefined,
820
- limit: typeof input?.limit === "number" ? input.limit : undefined,
821
- });
822
- }
823
- if (command === "dream:recent") {
824
- if (!deps.readModels) {
825
- return {
826
- ok: false,
827
- error: {
828
- code: "READ_MODELS_UNAVAILABLE",
829
- message: "dream:recent requires workspace read models",
830
- nextStep: "wire_read_models_into_ops_router",
831
- },
832
- };
833
- }
834
- const limit = typeof input?.limit === "number" ? input.limit : 5;
835
- const data = await deps.readModels.loadDreamRecent(limit);
836
- return { ok: true, data };
837
- }
838
- if (command === "cycle:recent") {
839
- if (!deps.readModels) {
840
- return {
841
- ok: false,
842
- error: {
843
- code: "READ_MODELS_UNAVAILABLE",
844
- message: "cycle:recent requires workspace read models",
845
- nextStep: "wire_read_models_into_ops_router",
846
- },
847
- };
848
- }
849
- const limit = typeof input?.limit === "number" ? input.limit : 5;
850
- const data = await deps.readModels.loadCycleRecent(limit);
851
- return { ok: true, data };
852
- }
853
- // ─── v7 commands (T-ROS.C.1) ─────────────────────────────────────────
854
- /** [G2] self_health — transparent pass-through from SelfHealthSnapshot (DR-042). */
855
- if (command === "self_health") {
856
- const generatedAt = new Date().toISOString();
857
- try {
858
- ensureMinimumProbes();
859
- const snap = await getSelfHealthSnapshot();
860
- const degraded_dimensions = Object.entries(snap.dimensions)
861
- .filter(([, d]) => d.status === "degraded")
862
- .map(([k]) => k);
863
- const envelope = {
864
- ok: true,
865
- command: "self_health",
866
- runtimeMode: "workspace_full_runtime",
867
- surfaceMode: "cli",
868
- generatedAt,
869
- data: {
870
- overall: snap.overall,
871
- generatedAt: snap.generatedAt,
872
- degraded_dimensions,
873
- dimensions: snap.dimensions,
874
- },
875
- warnings: [],
876
- sourceRefs: ["observability/services/self-health-snapshot.ts"],
877
- };
878
- return envelope;
879
- }
880
- catch (err) {
881
- const msg = err instanceof Error ? err.message : String(err);
882
- const envelope = {
883
- ok: false,
884
- command: "self_health",
885
- runtimeMode: "unavailable",
886
- surfaceMode: "cli",
887
- generatedAt,
888
- error: { code: "SELF_HEALTH_PROBE_FAILED", message: msg },
889
- warnings: [],
890
- sourceRefs: [],
891
- };
892
- return envelope;
893
- }
894
- }
895
- /**
896
- * [G3] tool_affordance — body-tool AffordanceMap pass-through.
897
- * Port not yet wired in this wave; returns degraded view with clear next-step.
898
- */
899
- if (command === "tool_affordance") {
900
- const generatedAt = new Date().toISOString();
901
- if (deps.toolAffordancePort) {
902
- const allStatuses = [
903
- "safe",
904
- "exploratory",
905
- "needs_auth",
906
- "painful",
907
- "unavailable",
908
- ];
909
- const platformIds = Array.isArray(input?.platformIds)
910
- ? input.platformIds.filter((item) => typeof item === "string")
911
- : typeof input?.platformId === "string"
912
- ? [input.platformId]
913
- : undefined;
914
- const data = await deps.toolAffordancePort.assembleAffordanceMap({
915
- platformIds,
916
- allowedStatuses: allStatuses,
917
- goalKind: typeof input?.goalKind === "string" ? input.goalKind : undefined,
918
- });
919
- const envelope = {
920
- ok: true,
921
- command: "tool_affordance",
922
- runtimeMode: "workspace_full_runtime",
923
- surfaceMode: "cli",
924
- generatedAt,
925
- data,
926
- warnings: [],
927
- sourceRefs: [
928
- "core/second-nature/body/tool-affordance/affordance-assembler.ts",
929
- ],
930
- };
931
- return envelope;
932
- }
933
- const envelope = {
934
- ok: false,
935
- command: "tool_affordance",
936
- runtimeMode: "unavailable",
937
- surfaceMode: "cli",
938
- generatedAt,
939
- error: {
940
- code: "TOOL_AFFORDANCE_PORT_UNWIRED",
941
- message: "tool_affordance requires body-tool AffordanceMap port (T-BTS.C.1) to be wired into OpsRouterDeps",
942
- nextStep: "wire_body_tool_port_into_ops_router_deps",
943
- },
944
- warnings: [],
945
- sourceRefs: [],
946
- };
947
- return envelope;
948
- }
949
- /**
950
- * [G6] heartbeat_digest — wraps generateHeartbeatDigest.
951
- * Requires auditStore in deps; degrades if unavailable.
952
- */
953
- if (command === "heartbeat_digest") {
954
- const generatedAt = new Date().toISOString();
955
- if (!deps.auditStore) {
956
- const envelope = {
957
- ok: false,
958
- command: "heartbeat_digest",
959
- runtimeMode: "unavailable",
960
- surfaceMode: "cli",
961
- generatedAt,
962
- error: {
963
- code: "AUDIT_STORE_UNAVAILABLE",
964
- message: "heartbeat_digest requires auditStore in OpsRouterDeps",
965
- nextStep: "wire_audit_store_into_ops_router",
966
- },
967
- warnings: [],
968
- sourceRefs: [],
969
- };
970
- return envelope;
971
- }
972
- const date = typeof input?.date === "string" && input.date
973
- ? input.date
974
- : new Date().toISOString().slice(0, 10);
975
- try {
976
- const digestDeps = {
977
- auditStore: deps.auditStore,
978
- ...deps.heartbeatDigestDeps,
979
- };
980
- const digest = await generateHeartbeatDigest(date, digestDeps);
981
- const envelope = {
982
- ok: true,
983
- command: "heartbeat_digest",
984
- runtimeMode: "workspace_full_runtime",
985
- surfaceMode: "cli",
986
- generatedAt,
987
- data: digest,
988
- warnings: [],
989
- sourceRefs: ["observability/services/heartbeat-digest-assembler.ts"],
990
- };
991
- return envelope;
992
- }
993
- catch (err) {
994
- const msg = err instanceof Error ? err.message : String(err);
995
- const envelope = {
996
- ok: false,
997
- command: "heartbeat_digest",
998
- runtimeMode: "unavailable",
999
- surfaceMode: "cli",
1000
- generatedAt,
1001
- error: { code: "DIGEST_GENERATION_FAILED", message: msg },
1002
- warnings: [],
1003
- sourceRefs: [],
1004
- };
1005
- return envelope;
1006
- }
1007
- }
1008
- /**
1009
- * [G6] snapshot:capture — production capture path for RestoreSnapshot +
1010
- * NarrativeTimeline. This gives restore and narrative:diff real state to consume.
1011
- */
1012
- if (command === "snapshot:capture") {
1013
- return captureRuntimeSnapshot(deps, input);
1014
- }
1015
- /**
1016
- * [G6] narrative:diff — queryNarrativeDiff between two versions.
1017
- * Requires narrativeTimelineDeps in OpsRouterDeps.
1018
- */
1019
- if (command === "narrative:diff") {
1020
- const generatedAt = new Date().toISOString();
1021
- if (!deps.narrativeTimelineDeps) {
1022
- const envelope = {
1023
- ok: false,
1024
- command: "narrative:diff",
1025
- runtimeMode: "unavailable",
1026
- surfaceMode: "cli",
1027
- generatedAt,
1028
- error: {
1029
- code: "NARRATIVE_TIMELINE_PORT_UNAVAILABLE",
1030
- message: "narrative:diff requires narrativeTimelineDeps in OpsRouterDeps",
1031
- nextStep: "wire_narrative_timeline_deps_into_ops_router",
1032
- },
1033
- warnings: [],
1034
- sourceRefs: [],
1035
- };
1036
- return envelope;
1037
- }
1038
- const fromVersion = typeof input?.from === "string" ? input.from : "";
1039
- const toVersion = typeof input?.to === "string" ? input.to : "";
1040
- if (!fromVersion || !toVersion) {
1041
- const envelope = {
1042
- ok: false,
1043
- command: "narrative:diff",
1044
- runtimeMode: "workspace_full_runtime",
1045
- surfaceMode: "cli",
1046
- generatedAt,
1047
- error: {
1048
- code: "MISSING_VERSIONS",
1049
- message: "narrative:diff requires 'from' and 'to' version arguments",
1050
- nextStep: "reinvoke_with_from_and_to",
1051
- },
1052
- warnings: [],
1053
- sourceRefs: [],
1054
- };
1055
- return envelope;
1056
- }
1057
- try {
1058
- const diff = await queryNarrativeDiff(fromVersion, toVersion, deps.narrativeTimelineDeps);
1059
- const envelope = {
1060
- ok: true,
1061
- command: "narrative:diff",
1062
- runtimeMode: "workspace_full_runtime",
1063
- surfaceMode: "cli",
1064
- generatedAt,
1065
- data: diff,
1066
- warnings: [],
1067
- sourceRefs: ["observability/services/narrative-timeline-query-service.ts"],
1068
- };
1069
- return envelope;
1070
- }
1071
- catch (err) {
1072
- if (err instanceof NarrativeVersionNotFoundError) {
1073
- const envelope = {
1074
- ok: false,
1075
- command: "narrative:diff",
1076
- runtimeMode: "workspace_full_runtime",
1077
- surfaceMode: "cli",
1078
- generatedAt,
1079
- error: {
1080
- code: "NARRATIVE_VERSION_NOT_FOUND",
1081
- message: err.message,
1082
- nextStep: "verify_version_exists_in_timeline",
1083
- },
1084
- warnings: [],
1085
- sourceRefs: [],
1086
- };
1087
- return envelope;
1088
- }
1089
- const msg = err instanceof Error ? err.message : String(err);
1090
- const envelope = {
1091
- ok: false,
1092
- command: "narrative:diff",
1093
- runtimeMode: "unavailable",
1094
- surfaceMode: "cli",
1095
- generatedAt,
1096
- error: { code: "NARRATIVE_DIFF_FAILED", message: msg },
1097
- warnings: [],
1098
- sourceRefs: [],
1099
- };
1100
- return envelope;
1101
- }
1102
- }
1103
- /**
1104
- * [G6] timeline — queryNarrativeTimeline with cursor pagination.
1105
- * Requires narrativeTimelineDeps in OpsRouterDeps.
1106
- */
1107
- if (command === "timeline") {
1108
- const generatedAt = new Date().toISOString();
1109
- if (!deps.narrativeTimelineDeps) {
1110
- const envelope = {
1111
- ok: false,
1112
- command: "timeline",
1113
- runtimeMode: "unavailable",
1114
- surfaceMode: "cli",
1115
- generatedAt,
1116
- error: {
1117
- code: "NARRATIVE_TIMELINE_PORT_UNAVAILABLE",
1118
- message: "timeline requires narrativeTimelineDeps in OpsRouterDeps",
1119
- nextStep: "wire_narrative_timeline_deps_into_ops_router",
1120
- },
1121
- warnings: [],
1122
- sourceRefs: [],
1123
- };
1124
- return envelope;
1125
- }
1126
- const now = new Date();
1127
- const to = typeof input?.to === "string" ? input.to : now.toISOString();
1128
- const from = typeof input?.from === "string"
1129
- ? input.from
1130
- : new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000).toISOString();
1131
- const limit = typeof input?.limit === "number" ? input.limit : 20;
1132
- const cursor = typeof input?.cursor === "string" ? input.cursor : undefined;
1133
- try {
1134
- const page = await queryNarrativeTimeline(from, to, { limit, cursor }, deps.narrativeTimelineDeps);
1135
- const envelope = {
1136
- ok: true,
1137
- command: "timeline",
1138
- runtimeMode: "workspace_full_runtime",
1139
- surfaceMode: "cli",
1140
- generatedAt,
1141
- data: page,
1142
- warnings: [],
1143
- sourceRefs: ["observability/services/narrative-timeline-query-service.ts"],
1144
- };
1145
- return envelope;
1146
- }
1147
- catch (err) {
1148
- const msg = err instanceof Error ? err.message : String(err);
1149
- const code = err.name === "NarrativeQueryRangeError"
1150
- ? "NARRATIVE_RANGE_EXCEEDED"
1151
- : "TIMELINE_QUERY_FAILED";
1152
- const envelope = {
1153
- ok: false,
1154
- command: "timeline",
1155
- runtimeMode: "unavailable",
1156
- surfaceMode: "cli",
1157
- generatedAt,
1158
- error: { code, message: msg },
1159
- warnings: [],
1160
- sourceRefs: [],
1161
- };
1162
- return envelope;
1163
- }
1164
- }
1165
- /**
1166
- * [G6] restore — bounded state restoration via RestoreSnapshotStore + audit (T-ROS.C.1, T-OBS.C.6).
1167
- * When restoreSnapshotStore is wired, attempts to apply the snapshot payload back to state.
1168
- * Always writes RestoreAudit. Never restores credential fields.
1169
- */
1170
- if (command === "restore") {
1171
- const generatedAt = new Date().toISOString();
1172
- if (!deps.auditStore) {
1173
- const envelope = {
1174
- ok: false,
1175
- command: "restore",
1176
- runtimeMode: "unavailable",
1177
- surfaceMode: "cli",
1178
- generatedAt,
1179
- error: {
1180
- code: "AUDIT_STORE_UNAVAILABLE",
1181
- message: "restore requires auditStore in OpsRouterDeps",
1182
- nextStep: "wire_audit_store_into_ops_router",
1183
- },
1184
- warnings: [],
1185
- sourceRefs: [],
1186
- };
1187
- return envelope;
1188
- }
1189
- let restoreTarget;
1190
- let fromVersion;
1191
- let toVersion;
1192
- // T-V7C.C.5: snapshotId operator-friendly parameter takes precedence over legacy fields.
1193
- // When snapshotId is provided, resolve restoreTarget/fromVersion/toVersion from the
1194
- // matching snapshot row; otherwise fall back to explicit legacy parameters.
1195
- const snapshotId = textInput(input, "snapshotId");
1196
- if (snapshotId) {
1197
- if (!deps.restoreSnapshotStore) {
1198
- const envelope = {
1199
- ok: false,
1200
- command: "restore",
1201
- runtimeMode: "unavailable",
1202
- surfaceMode: "cli",
1203
- generatedAt,
1204
- error: {
1205
- code: "RESTORE_SNAPSHOT_STORE_UNAVAILABLE",
1206
- message: "snapshotId restore requires restoreSnapshotStore in OpsRouterDeps",
1207
- nextStep: "wire_restore_snapshot_store_into_ops_router",
1208
- },
1209
- warnings: [],
1210
- sourceRefs: [],
1211
- };
1212
- return envelope;
1213
- }
1214
- const snapshots = await deps.restoreSnapshotStore.listSnapshots();
1215
- const match = snapshots.find((s) => s.snapshotId === snapshotId);
1216
- if (match) {
1217
- restoreTarget = snapshotId;
1218
- fromVersion = match.capturedAt;
1219
- toVersion = snapshotId;
1220
- }
1221
- else {
1222
- const envelope = {
1223
- ok: false,
1224
- command: "restore",
1225
- runtimeMode: "workspace_full_runtime",
1226
- surfaceMode: "cli",
1227
- generatedAt,
1228
- error: {
1229
- code: "SNAPSHOT_NOT_FOUND",
1230
- message: `snapshotId ${snapshotId} not found in restore_snapshot table`,
1231
- nextStep: "list_available_snapshots_or_verify_snapshotId",
1232
- },
1233
- warnings: [],
1234
- sourceRefs: [],
1235
- };
1236
- return envelope;
1237
- }
1238
- }
1239
- else {
1240
- const missingFields = [];
1241
- if (typeof input?.restoreTarget !== "string")
1242
- missingFields.push("restoreTarget");
1243
- if (typeof input?.fromVersion !== "string")
1244
- missingFields.push("fromVersion");
1245
- if (typeof input?.toVersion !== "string")
1246
- missingFields.push("toVersion");
1247
- if (missingFields.length > 0) {
1248
- const envelope = {
1249
- ok: false,
1250
- command: "restore",
1251
- runtimeMode: "workspace_full_runtime",
1252
- surfaceMode: "cli",
1253
- generatedAt,
1254
- error: {
1255
- code: "MISSING_RESTORE_FIELDS",
1256
- message: `restore requires: ${missingFields.join(", ")}`,
1257
- nextStep: "reinvoke_with_required_fields",
1258
- },
1259
- warnings: [],
1260
- sourceRefs: [],
1261
- };
1262
- return envelope;
1263
- }
1264
- restoreTarget = input.restoreTarget;
1265
- fromVersion = input.fromVersion;
1266
- toVersion = input.toVersion;
1267
- }
1268
- // [NEW] Invoke bounded restore via RestoreSnapshotStore when wired
1269
- let restoreResult = {
1270
- ok: false,
1271
- completedEntities: [],
1272
- failedEntities: [],
1273
- warnings: ["restore_snapshot_store_unavailable"],
1274
- };
1275
- if (deps.restoreSnapshotStore) {
1276
- restoreResult = await deps.restoreSnapshotStore.applyBoundedRestore({
1277
- restoreTarget: restoreTarget,
1278
- fromVersion: fromVersion,
1279
- toVersion: toVersion,
1280
- });
1281
- }
1282
- const event = {
1283
- id: `restore-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
1284
- restoreTarget: restoreTarget,
1285
- fromVersion: fromVersion,
1286
- toVersion: toVersion,
1287
- triggeredBy: input?.triggeredBy ?? "operator",
1288
- reason: typeof input?.reason === "string" ? input.reason : "manual_restore",
1289
- completedEntities: restoreResult.completedEntities,
1290
- failedEntities: restoreResult.failedEntities,
1291
- // credentials are always excluded from restore audit
1292
- excludedFields: Array.isArray(input?.excludedFields)
1293
- ? input.excludedFields.filter((f) => typeof f === "string")
1294
- : ["credential", "encryptionKey"],
1295
- restoredFieldCount: restoreResult.completedEntities.length,
1296
- createdAt: generatedAt,
1297
- traceId: typeof input?.traceId === "string" ? input.traceId : `trace-restore-${Date.now()}`,
1298
- };
1299
- const auditResult = await writeRestoreAudit(event, deps.auditStore);
1300
- const envelope = {
1301
- ok: restoreResult.ok && auditResult.ok,
1302
- command: "restore",
1303
- runtimeMode: "workspace_full_runtime",
1304
- surfaceMode: "cli",
1305
- generatedAt,
1306
- data: {
1307
- auditWritten: auditResult.warnings.length === 0,
1308
- fromVersion: event.fromVersion,
1309
- toVersion: event.toVersion,
1310
- restoreTarget: event.restoreTarget,
1311
- isPartialRestore: event.failedEntities.length > 0,
1312
- failedEntities: event.failedEntities,
1313
- completedEntities: event.completedEntities,
1314
- restoreSnapshotStoreAvailable: !!deps.restoreSnapshotStore,
1315
- },
1316
- warnings: [...restoreResult.warnings, ...auditResult.warnings],
1317
- sourceRefs: [
1318
- "observability/services/restore-audit-service.ts",
1319
- "storage/services/restore-snapshot-store.ts",
1320
- ],
1321
- };
1322
- return envelope;
1323
- }
1324
- /**
1325
- * [G7] runtime_secret_bootstrap — RuntimeSecretAnchorView pass-through.
1326
- * Requires secretAnchorDeps in OpsRouterDeps; never returns key plaintext.
1327
- */
1328
- if (command === "runtime_secret_bootstrap") {
1329
- const generatedAt = new Date().toISOString();
1330
- if (!deps.secretAnchorDeps) {
1331
- const envelope = {
1332
- ok: false,
1333
- command: "runtime_secret_bootstrap",
1334
- runtimeMode: "unavailable",
1335
- surfaceMode: "cli",
1336
- generatedAt,
1337
- error: {
1338
- code: "SECRET_ANCHOR_DEPS_UNAVAILABLE",
1339
- message: "runtime_secret_bootstrap requires secretAnchorDeps in OpsRouterDeps",
1340
- nextStep: "wire_secret_anchor_deps_into_ops_router",
1341
- },
1342
- warnings: [],
1343
- sourceRefs: [],
1344
- };
1345
- return envelope;
1346
- }
1347
- try {
1348
- const view = await viewSecretAnchor(deps.secretAnchorDeps);
1349
- // Map to RuntimeSecretBootstrapView (design model §6.1)
1350
- const data = {
1351
- status: view.status === "verified" || view.status === "ok"
1352
- ? "ok"
1353
- : view.status === "missing"
1354
- ? "runtime_secret_anchor_missing"
1355
- : view.status === "wrong_key"
1356
- ? "credential_recovery_required"
1357
- : view.status === "decryption_failed"
1358
- ? "runtime_secret_unavailable"
1359
- : "unknown",
1360
- keyHealth: view.status === "verified" || view.status === "ok"
1361
- ? "ok"
1362
- : view.status === "missing"
1363
- ? "missing_key"
1364
- : view.status === "wrong_key"
1365
- ? "wrong_key"
1366
- : "unknown",
1367
- anchorLocation: view.keyPath,
1368
- recoveryPrincipleRef: view.recoveryDocRef,
1369
- plaintextKeyExposed: false,
1370
- reasonCode: view.reasonCode,
1371
- recoverySteps: view.recoverySteps,
1372
- };
1373
- const envelope = {
1374
- ok: true,
1375
- command: "runtime_secret_bootstrap",
1376
- runtimeMode: "workspace_full_runtime",
1377
- surfaceMode: "cli",
1378
- generatedAt,
1379
- data,
1380
- warnings: [],
1381
- sourceRefs: ["observability/services/runtime-secret-anchor-view.ts"],
1382
- };
1383
- return envelope;
1384
- }
1385
- catch (err) {
1386
- const msg = err instanceof Error ? err.message : String(err);
1387
- const envelope = {
1388
- ok: false,
1389
- command: "runtime_secret_bootstrap",
1390
- runtimeMode: "unavailable",
1391
- surfaceMode: "cli",
1392
- generatedAt,
1393
- error: { code: "SECRET_ANCHOR_PROBE_FAILED", message: msg },
1394
- warnings: [],
1395
- sourceRefs: [],
1396
- };
1397
- return envelope;
1398
- }
1399
- }
1400
- // ─── T-V7C.C.4R: guidance_payload ──────────────────────────────────────
1401
- // Returns the assembled impulse + atmosphere for a given scene context.
1402
- // Useful for Claw to inspect what guidance content would be injected before
1403
- // a real heartbeat cycle, and to verify platform-specific impulse overrides.
1404
- if (command === "guidance_payload") {
1405
- const generatedAt = new Date().toISOString();
1406
- const { assembleImpulseSync } = await import("../../guidance/impulse-assembler.js");
1407
- const { getBaselineAtmosphereTemplate } = await import("../../guidance/template-registry.js");
1408
- const sceneType = input?.sceneType ?? "social";
1409
- const capabilityIntent = typeof input?.capabilityIntent === "string"
1410
- ? input.capabilityIntent
1411
- : undefined;
1412
- const platformId = typeof input?.platformId === "string"
1413
- ? input.platformId
1414
- : undefined;
1415
- const validSceneTypes = ["social", "reply", "outreach", "quiet", "explain", "user_reply"];
1416
- if (!validSceneTypes.includes(sceneType)) {
1417
- const envelope = {
1418
- ok: false,
1419
- command: "guidance_payload",
1420
- runtimeMode: "unavailable",
1421
- surfaceMode: "cli",
1422
- generatedAt,
1423
- error: {
1424
- code: "INVALID_SCENE_TYPE",
1425
- message: `sceneType must be one of: ${validSceneTypes.join(", ")}`,
1426
- nextStep: "reinvoke_with_valid_scene_type",
1427
- },
1428
- warnings: [],
1429
- sourceRefs: [],
1430
- };
1431
- return envelope;
1432
- }
1433
- const impulseResult = assembleImpulseSync({
1434
- sceneType: sceneType,
1435
- capabilityIntent,
1436
- platformId,
1437
- });
1438
- const { buildExpressionBoundary } = await import("../../guidance/output-guard.js");
1439
- const { getShortAtmosphereTemplate } = await import("../../guidance/template-registry.js");
1440
- const atmosphere = getShortAtmosphereTemplate("active", "low");
1441
- const expressionBoundary = buildExpressionBoundary(sceneType);
1442
- const envelope = {
1443
- ok: true,
1444
- command: "guidance_payload",
1445
- runtimeMode: deps.runtimeAvailable ? "workspace_full_runtime" : "host_safe_carrier",
1446
- surfaceMode: "cli",
1447
- generatedAt,
1448
- data: {
1449
- sceneType,
1450
- capabilityIntent: capabilityIntent ?? null,
1451
- platformId: platformId ?? null,
1452
- capabilityClass: impulseResult.capabilityClass,
1453
- impulseSource: impulseResult.source,
1454
- impulseText: impulseResult.impulse?.text ?? null,
1455
- impulseReviewStatus: impulseResult.impulse?.reviewStatus ?? null,
1456
- atmosphereText: atmosphere.text,
1457
- atmosphereReviewStatus: atmosphere.reviewStatus,
1458
- expressionBoundaryConstraints: expressionBoundary.constraints,
1459
- expressionBoundaryStyle: expressionBoundary.style,
1460
- },
1461
- warnings: impulseResult.source === "none"
1462
- ? ["no_impulse_available_for_this_scene_and_capability"]
1463
- : [],
1464
- sourceRefs: [
1465
- "guidance/capability-class.ts",
1466
- "guidance/impulse-assembler.ts",
1467
- "guidance/template-registry.ts",
1468
- "guidance/output-guard.ts",
1469
- ],
1470
- };
1471
- return envelope;
1472
- }
1473
- return {
1474
- ok: false,
1475
- error: {
1476
- code: "unknown_ops_command",
1477
- message: `Unknown ops command: ${command}`,
1478
- },
1479
- };
1480
- },
1481
- };
1482
- }
1
+ /**
2
+ * Shared ops command dispatch for CLI + tool surfaces (T1.1.3, T1.2.2).
3
+ *
4
+ * v7 additions (T-ROS.C.1): self_health, tool_affordance, connector_test --wet,
5
+ * heartbeat_digest, narrative:diff, timeline, restore, runtime_secret_bootstrap.
6
+ * All commands return RuntimeOpsEnvelope.
7
+ */
8
+ import { createHash } from "node:crypto";
9
+ import fs from "node:fs";
10
+ import path from "node:path";
11
+ import { heartbeatCheck, } from "./heartbeat-surface.js";
12
+ import { showOperatorFallback, OperatorFallbackNotFoundError, } from "./show-operator-fallback.js";
13
+ import { probeHostCapability } from "../host-capability/probe-host-capability.js";
14
+ import { recordHostCapability } from "../host-capability/record-host-capability.js";
15
+ import { runNearRealConnectorSmoke } from "../../connectors/near-real/near-real-connector-smoke.js";
16
+ import { scanConnectorManifests } from "../../connectors/registry/manifest-scanner.js";
17
+ import { parseConnectorManifestV6 } from "../../connectors/manifest/manifest-parser.js";
18
+ import { connectorInit } from "../commands/connector-init.js";
19
+ import { connectorBehaviorAdd } from "../commands/connector-behavior.js";
20
+ import { connectorStatus, connectorTest } from "../commands/connector-status.js";
21
+ import { goalCommand } from "../commands/goal.js";
22
+ // v7 observability services (T-ROS.C.1)
23
+ import { getSelfHealthSnapshot, ensureMinimumProbes, } from "../../observability/services/self-health-snapshot.js";
24
+ import { generateHeartbeatDigest, } from "../../observability/services/heartbeat-digest-assembler.js";
25
+ import { queryNarrativeTimeline, queryNarrativeDiff, NarrativeVersionNotFoundError, } from "../../observability/services/narrative-timeline-query-service.js";
26
+ import { viewSecretAnchor, } from "../../observability/services/runtime-secret-anchor-view.js";
27
+ import { writeRestoreAudit, } from "../../observability/services/restore-audit-service.js";
28
+ import { createHistoryDigestStore } from "../../storage/services/history-digest-store.js";
29
+ // v8 T-ROS.C.1: loop_status read model
30
+ import { readLoopStatus } from "../../observability/loop-status.js";
31
+ // T-ROS.C.3: ManualRunDispatcher and its deps
32
+ import { createManualRunDispatcher, } from "./manual-run-dispatcher.js";
33
+ import { createExperienceWriter } from "../../core/second-nature/body/tool-experience/experience-writer.js";
34
+ import { createCapabilityProbeResultStore, createToolExperienceStore, } from "../../storage/services/tool-experience-store.js";
35
+ import { createWetProbeRunner } from "../../connectors/base/wet-probe-runner.js";
36
+ import { CapabilityContractRegistryV7 } from "../../connectors/base/manifest-v7.js";
37
+ // v7 T-V7C.C.6: Dream scheduling deps for heartbeat_check quiet→dream auto-trigger
38
+ import { scheduleDream } from "../../dream/dream-scheduler.js";
39
+ import { createDreamInputLoader } from "../../dream/dream-input-loader.js";
40
+ import { createDiaryDreamStore } from "../../storage/services/diary-dream-store.js";
41
+ // v7 T-CP.C.3 / T-BTS.C.5: heartbeat loop policies and breaker
42
+ import { createGoalLifecyclePolicy } from "../../core/second-nature/heartbeat/goal-lifecycle-policy.js";
43
+ import { createIdleCuriosityPolicy } from "../../core/second-nature/heartbeat/idle-curiosity-policy.js";
44
+ import { createCircuitBreakerManager } from "../../core/second-nature/body/circuit-breaker/circuit-breaker-manager.js";
45
+ import { createProbeSignalAdapter } from "../../core/second-nature/body/probe-signal-adapter.js";
46
+ function coerceProbeOnlyFlag(input) {
47
+ const v = input?.probeOnly;
48
+ return v === true || v === "true" || v === 1 || v === "1";
49
+ }
50
+ /**
51
+ * v7 T-V7C.C.6: Build a minimal QuietDreamSchedulePort backed by the state DB.
52
+ * When a source-backed Quiet write completes, this port triggers Dream scheduling
53
+ * via the standard scheduleDream path (rules-only mode when no model port).
54
+ */
55
+ function createQuietDreamSchedulePort(state) {
56
+ return {
57
+ async scheduleDream({ triggerKind, runId, traceId }) {
58
+ const dreamStore = createDiaryDreamStore(state);
59
+ const inputLoader = createDreamInputLoader({ database: state });
60
+ const statePort = {
61
+ async loadDreamInputs(query) {
62
+ return inputLoader.loadDreamInputs(query);
63
+ },
64
+ async writeDreamOutput(output) {
65
+ // Bridge: dream-engine emits dream/types DreamOutput; diary-dream-store expects shared/types.
66
+ // Structures are identical at runtime; TS strictness requires the cast.
67
+ await dreamStore.appendDreamOutput(output);
68
+ return { outputId: output.outputId, status: "acknowledged" };
69
+ },
70
+ async markDreamOutputLifecycle(input) {
71
+ // transitionDreamOutputLifecycle only accepts accepted|archived.
72
+ if (input.newStatus !== "accepted" && input.newStatus !== "archived") {
73
+ return { outputId: input.outputId, status: "degraded" };
74
+ }
75
+ await dreamStore.transitionDreamOutputLifecycle(input.outputId, input.newStatus);
76
+ return { outputId: input.outputId, status: "acknowledged" };
77
+ },
78
+ };
79
+ const result = await scheduleDream({
80
+ triggerKind,
81
+ runId,
82
+ traceId,
83
+ statePort,
84
+ windowKey: "quiet_completion",
85
+ });
86
+ return { status: result.status, reason: result.reason };
87
+ },
88
+ };
89
+ }
90
+ const SNAPSHOT_TABLE_BY_KIND = {
91
+ identity_profile: "identity_profile",
92
+ agent_goal: "agent_goal",
93
+ tool_experience: "tool_experience",
94
+ daily_diary: "daily_diary_index",
95
+ dream_output: "dream_output_index",
96
+ narrative_timeline: "narrative_timeline",
97
+ };
98
+ const DEFAULT_SNAPSHOT_KINDS = [
99
+ "identity_profile",
100
+ "agent_goal",
101
+ "tool_experience",
102
+ "daily_diary",
103
+ "dream_output",
104
+ "narrative_timeline",
105
+ ];
106
+ function coerceRestorableKinds(value) {
107
+ if (!Array.isArray(value))
108
+ return undefined;
109
+ const valid = new Set(DEFAULT_SNAPSHOT_KINDS);
110
+ return value.filter((item) => typeof item === "string" && valid.has(item));
111
+ }
112
+ function tableExists(state, table) {
113
+ const result = state.sqlite.exec(`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ? LIMIT 1`, [table]);
114
+ return result.length > 0 && result[0].values.length > 0;
115
+ }
116
+ function readRowsFromTable(state, table) {
117
+ const result = state.sqlite.exec(`SELECT * FROM ${table}`);
118
+ if (result.length === 0 || result[0].values.length === 0)
119
+ return [];
120
+ const columns = result[0].columns;
121
+ return result[0].values.map((row) => {
122
+ const out = {};
123
+ columns.forEach((column, index) => {
124
+ out[column] = row[index];
125
+ });
126
+ return out;
127
+ });
128
+ }
129
+ function stringArray(value) {
130
+ return Array.isArray(value)
131
+ ? value.filter((item) => typeof item === "string")
132
+ : [];
133
+ }
134
+ function textInput(input, key) {
135
+ const value = input?.[key];
136
+ if (typeof value !== "string")
137
+ return undefined;
138
+ const trimmed = value.trim();
139
+ return trimmed.length > 0 ? trimmed : undefined;
140
+ }
141
+ function buildSnapshotNarrativeDelta(input, snapshotId, rowCounts) {
142
+ const explicit = input?.narrativeSnapshot &&
143
+ typeof input.narrativeSnapshot === "object" &&
144
+ !Array.isArray(input.narrativeSnapshot)
145
+ ? input.narrativeSnapshot
146
+ : {};
147
+ const from = (key) => input?.[key] ?? explicit[key];
148
+ const sourceRefs = stringArray(from("sourceRefs"));
149
+ return {
150
+ focus: from("focus") ?? "workspace_state",
151
+ progress: from("progress") ??
152
+ `snapshot_captured:${Object.entries(rowCounts)
153
+ .map(([kind, count]) => `${kind}=${count}`)
154
+ .join(",")}`,
155
+ nextIntent: from("nextIntent") ?? "restore_ready",
156
+ toneSignal: from("toneSignal") ?? "system_maintenance",
157
+ acceptedGoalId: from("acceptedGoalId") ?? undefined,
158
+ sourceRefs: sourceRefs.length > 0
159
+ ? sourceRefs
160
+ : [`restore_snapshot:${snapshotId}`, "runtime_ops:snapshot_capture"],
161
+ reasonCode: from("reasonCode") ?? "snapshot_captured",
162
+ summaryText: from("summaryText") ?? `Captured restore snapshot ${snapshotId}`,
163
+ };
164
+ }
165
+ function hashNarrativeSnapshot(input) {
166
+ return createHash("sha256")
167
+ .update(JSON.stringify({
168
+ previousHash: input.previousHash,
169
+ snapshotId: input.snapshotId,
170
+ delta: input.delta,
171
+ createdAt: input.createdAt,
172
+ }))
173
+ .digest("hex");
174
+ }
175
+ function resolveManifestPath(manifestPath, workspaceRoot) {
176
+ if (path.isAbsolute(manifestPath))
177
+ return manifestPath;
178
+ return path.join(workspaceRoot ?? process.cwd(), manifestPath);
179
+ }
180
+ function registerConnectorForWetProbe(input) {
181
+ if (input.entry.manifestPath) {
182
+ try {
183
+ const manifestText = fs.readFileSync(resolveManifestPath(input.entry.manifestPath, input.workspaceRoot), "utf-8");
184
+ const parsed = JSON.parse(manifestText);
185
+ const registered = input.registryV7.register(parsed);
186
+ if (registered.ok && input.registryV7.hasCapability(input.entry.platformId, input.selectedCapabilityId)) {
187
+ return;
188
+ }
189
+ }
190
+ catch {
191
+ // Non-v7 or YAML workspace manifests are projected below.
192
+ }
193
+ }
194
+ input.registryV7.register({
195
+ platformId: input.entry.platformId,
196
+ capabilities: input.entry.capabilities.map((capabilityId) => ({
197
+ capabilityId,
198
+ intent: capabilityId,
199
+ probeConfig: capabilityId === input.selectedCapabilityId && input.safeEndpoint
200
+ ? {
201
+ safeEndpoint: input.safeEndpoint,
202
+ idempotencyClass: "read_only",
203
+ }
204
+ : undefined,
205
+ })),
206
+ channelPriority: ["runtime_ops"],
207
+ credentialTypes: ["runtime_ops_probe"],
208
+ });
209
+ }
210
+ async function captureRuntimeSnapshot(deps, input) {
211
+ const generatedAt = new Date().toISOString();
212
+ if (!deps.state || !deps.restoreSnapshotStore) {
213
+ return {
214
+ ok: false,
215
+ command: "snapshot:capture",
216
+ runtimeMode: "unavailable",
217
+ surfaceMode: "cli",
218
+ generatedAt,
219
+ error: {
220
+ code: "SNAPSHOT_CAPTURE_DEPS_UNAVAILABLE",
221
+ message: "snapshot:capture requires state DB and RestoreSnapshotStore in OpsRouterDeps",
222
+ nextStep: "wire_state_and_restore_snapshot_store_into_ops_router",
223
+ },
224
+ warnings: [],
225
+ sourceRefs: [],
226
+ };
227
+ }
228
+ const snapshotId = textInput(input, "snapshotId") ??
229
+ `snapshot:${Date.now()}:${Math.random().toString(36).slice(2, 8)}`;
230
+ const requestedKinds = coerceRestorableKinds(input?.entityWhitelist) ?? [...DEFAULT_SNAPSHOT_KINDS];
231
+ const rowCounts = {};
232
+ const warnings = [];
233
+ for (const kind of requestedKinds) {
234
+ const table = SNAPSHOT_TABLE_BY_KIND[kind];
235
+ if (!tableExists(deps.state, table)) {
236
+ rowCounts[kind] = 0;
237
+ warnings.push(`table_missing:${kind}:${table}`);
238
+ continue;
239
+ }
240
+ rowCounts[kind] = readRowsFromTable(deps.state, table).length;
241
+ }
242
+ const historyStore = createHistoryDigestStore(deps.state);
243
+ const previousHash = (await historyStore.listNarrativeTimeline({ limit: 1 }))[0]?.currentHash ?? "";
244
+ const delta = buildSnapshotNarrativeDelta(input, snapshotId, rowCounts);
245
+ const currentHash = hashNarrativeSnapshot({
246
+ previousHash,
247
+ snapshotId,
248
+ delta,
249
+ createdAt: generatedAt,
250
+ });
251
+ await historyStore.appendNarrativeTimeline({
252
+ timelineId: snapshotId,
253
+ entryType: "owner.override",
254
+ subjectId: textInput(input, "subjectId") ?? snapshotId,
255
+ delta,
256
+ previousHash,
257
+ currentHash,
258
+ createdAt: generatedAt,
259
+ });
260
+ const payload = {};
261
+ const capturedKinds = [];
262
+ for (const kind of requestedKinds) {
263
+ const table = SNAPSHOT_TABLE_BY_KIND[kind];
264
+ if (!tableExists(deps.state, table))
265
+ continue;
266
+ const rows = readRowsFromTable(deps.state, table);
267
+ rowCounts[kind] = rows.length;
268
+ if (rows.length > 0) {
269
+ payload[kind] = rows;
270
+ capturedKinds.push(kind);
271
+ }
272
+ }
273
+ const snapshot = await deps.restoreSnapshotStore.captureSnapshot({
274
+ snapshotId,
275
+ entityWhitelist: requestedKinds,
276
+ payload,
277
+ capturedAt: generatedAt,
278
+ });
279
+ return {
280
+ ok: true,
281
+ command: "snapshot:capture",
282
+ runtimeMode: "workspace_full_runtime",
283
+ surfaceMode: "cli",
284
+ generatedAt,
285
+ data: {
286
+ snapshotId: snapshot.snapshotId,
287
+ capturedAt: snapshot.capturedAt,
288
+ entityWhitelist: snapshot.entityWhitelist,
289
+ capturedKinds,
290
+ rowCounts,
291
+ narrativeVersion: snapshotId,
292
+ },
293
+ warnings,
294
+ sourceRefs: [
295
+ "storage/services/restore-snapshot-store.ts",
296
+ "storage/services/history-digest-store.ts",
297
+ ],
298
+ };
299
+ }
300
+ /**
301
+ * T1.2.8 — static local adapter: all checks return `unknown` when no real host is available.
302
+ * Allows `capability_probe` to be called from CLI / workspace bridge without requiring a live host.
303
+ */
304
+ function createStaticUnknownAdapter(workspaceRoot) {
305
+ const now = new Date().toISOString();
306
+ const unknownResult = (name) => ({
307
+ name,
308
+ verdict: "unknown",
309
+ observedAt: now,
310
+ reason: "static_local_probe_no_host_context",
311
+ evidenceRefs: [],
312
+ });
313
+ function checkDeliveryTarget() {
314
+ if (!workspaceRoot) {
315
+ return { status: "target_none", evidenceRefs: [], reason: "no_workspace_root_provided" };
316
+ }
317
+ const deliveryCapabilities = ["message.send", "comment.reply"];
318
+ const scanned = scanConnectorManifests(workspaceRoot);
319
+ for (const manifestFile of scanned) {
320
+ const parsed = parseConnectorManifestV6(manifestFile.content, manifestFile.path);
321
+ if (parsed.ok && parsed.manifest.capabilities.some((cap) => deliveryCapabilities.includes(cap.id))) {
322
+ return {
323
+ status: "target_available",
324
+ evidenceRefs: [
325
+ {
326
+ id: `delivery:${parsed.manifest.platformId}`,
327
+ kind: "workspace_artifact",
328
+ uri: `workspace://connectors/${parsed.manifest.platformId}/manifest.yaml`,
329
+ observedAt: now,
330
+ },
331
+ ],
332
+ };
333
+ }
334
+ }
335
+ return { status: "target_none", evidenceRefs: [], reason: "no_delivery_connector_found_in_workspace" };
336
+ }
337
+ return {
338
+ checkPluginLoad: () => unknownResult("plugin_load"),
339
+ checkHeartbeatBridge: () => unknownResult("heartbeat_bridge"),
340
+ checkHeartbeatToolInvocation: () => unknownResult("heartbeat_tool_invocation"),
341
+ checkDeliveryTarget,
342
+ checkAckDropBehavior: () => unknownResult("ack_drop"),
343
+ checkHookSupport: () => [],
344
+ };
345
+ }
346
+ export function createOpsRouter(deps) {
347
+ return {
348
+ heartbeatCheck: (input) => heartbeatCheck({
349
+ ...input,
350
+ runtimeAvailable: input.runtimeAvailable ?? deps.runtimeAvailable,
351
+ readModels: input.readModels ?? deps.readModels,
352
+ runtimeRecorder: input.runtimeRecorder ?? deps.runtimeRecorder,
353
+ state: input.state ?? deps.state,
354
+ workspaceRoot: input.workspaceRoot ?? deps.workspaceRoot,
355
+ connectorExecutor: input.connectorExecutor ?? deps.connectorExecutor,
356
+ connectorRegistry: input
357
+ ?.connectorRegistry ?? deps.connectorRegistry,
358
+ digestOpts: input.digestOpts,
359
+ dreamSchedulePort: input.dreamSchedulePort,
360
+ }),
361
+ async dispatch(command, input) {
362
+ if (command === "heartbeat_check") {
363
+ const runtimeAvailable = typeof input?.runtimeAvailable === "boolean"
364
+ ? input.runtimeAvailable
365
+ : deps.runtimeAvailable;
366
+ // v7 T-V7C.C.2: assemble affordance map and experience writer for breaker-aware heartbeat.
367
+ let affordanceMap;
368
+ if (deps.toolAffordancePort) {
369
+ try {
370
+ affordanceMap = await deps.toolAffordancePort.assembleAffordanceMap({});
371
+ }
372
+ catch {
373
+ // degrade gracefully; guard-layer will skip breaker check without affordanceMap
374
+ }
375
+ }
376
+ let experienceWriter;
377
+ if (deps.state) {
378
+ experienceWriter = createExperienceWriter(createToolExperienceStore(deps.state));
379
+ }
380
+ // v7 T-V7C.C.6: assemble digest opts when auditStore is wired.
381
+ let digestOpts;
382
+ if (deps.auditStore) {
383
+ digestOpts = {
384
+ assemblerDeps: {
385
+ auditStore: deps.auditStore,
386
+ ...deps.heartbeatDigestDeps,
387
+ },
388
+ };
389
+ }
390
+ // v7 T-V7C.C.6: assemble dream schedule port when state DB is wired.
391
+ let dreamSchedulePort;
392
+ if (deps.state) {
393
+ dreamSchedulePort = createQuietDreamSchedulePort(deps.state);
394
+ }
395
+ // v7 T-CP.C.3: assemble goal lifecycle and idle curiosity policies.
396
+ const goalLifecyclePolicy = createGoalLifecyclePolicy();
397
+ const idleCuriosityPolicy = createIdleCuriosityPolicy();
398
+ // v7 T-BTS.C.5: assemble circuit breaker manager when state DB is wired.
399
+ let circuitBreakerManager;
400
+ if (deps.state) {
401
+ const probeResultStore = createCapabilityProbeResultStore(deps.state);
402
+ const toolExpStore = createToolExperienceStore(deps.state);
403
+ const probeAdapter = createProbeSignalAdapter({
404
+ wetProbeRunner: createWetProbeRunner(),
405
+ probeResultStore,
406
+ toolExperienceStore: toolExpStore,
407
+ });
408
+ const registryV7 = new CapabilityContractRegistryV7();
409
+ circuitBreakerManager = createCircuitBreakerManager({
410
+ database: deps.state,
411
+ probeAdapter,
412
+ registry: registryV7,
413
+ });
414
+ }
415
+ try {
416
+ const result = await heartbeatCheck({
417
+ probeOnly: coerceProbeOnlyFlag(input),
418
+ runtimeAvailable,
419
+ fakeControlPlanePassthrough: input?.fakeControlPlanePassthrough &&
420
+ typeof input.fakeControlPlanePassthrough === "object"
421
+ ? input.fakeControlPlanePassthrough
422
+ : undefined,
423
+ readModels: input?.readModels ??
424
+ deps.readModels,
425
+ runtimeRecorder: input
426
+ ?.runtimeRecorder ?? deps.runtimeRecorder,
427
+ state: input?.state ??
428
+ deps.state,
429
+ workspaceRoot: input
430
+ ?.workspaceRoot ?? deps.workspaceRoot,
431
+ timestamp: typeof input?.timestamp === "string" ? input.timestamp : undefined,
432
+ sessionContext: typeof input?.sessionContext === "string"
433
+ ? input.sessionContext
434
+ : undefined,
435
+ scopeHint: input?.scopeHint,
436
+ connectorExecutor: input
437
+ ?.connectorExecutor ?? deps.connectorExecutor,
438
+ connectorRegistry: input
439
+ ?.connectorRegistry ?? deps.connectorRegistry,
440
+ affordanceMap,
441
+ experienceWriter,
442
+ digestOpts,
443
+ dreamSchedulePort,
444
+ goalLifecyclePolicy,
445
+ idleCuriosityPolicy,
446
+ circuitBreakerManager,
447
+ });
448
+ if (result.ok &&
449
+ result.surfaceMode === "workspace_full_runtime" &&
450
+ !coerceProbeOnlyFlag(input) &&
451
+ deps.state &&
452
+ deps.restoreSnapshotStore) {
453
+ try {
454
+ const capture = await captureRuntimeSnapshot(deps, {
455
+ snapshotId: `heartbeat:${result.decisionId ?? "cycle"}:${Date.now()}`,
456
+ subjectId: result.decisionId ?? "heartbeat_check",
457
+ reasonCode: "heartbeat_check",
458
+ summaryText: `Heartbeat ${result.status} captured bounded restore snapshot`,
459
+ focus: result.status,
460
+ progress: result.reasons.join(",") || "heartbeat_completed",
461
+ nextIntent: "continue_runtime_loop",
462
+ sourceRefs: result.decisionId
463
+ ? [`heartbeat:${result.decisionId}`]
464
+ : ["heartbeat:runtime"],
465
+ });
466
+ if (capture.ok) {
467
+ result.reasons = [...result.reasons, "restore_snapshot_captured"];
468
+ }
469
+ }
470
+ catch (err) {
471
+ const msg = err instanceof Error ? err.message : String(err);
472
+ result.reasons = [...result.reasons, `restore_snapshot_capture_failed:${msg}`];
473
+ }
474
+ }
475
+ return result;
476
+ }
477
+ catch (err) {
478
+ const msg = err instanceof Error ? err.message : String(err);
479
+ const envelope = {
480
+ ok: false,
481
+ command: "heartbeat_check",
482
+ runtimeMode: runtimeAvailable ? "workspace_full_runtime" : "unavailable",
483
+ surfaceMode: "cli",
484
+ generatedAt: new Date().toISOString(),
485
+ error: {
486
+ code: "HEARTBEAT_CYCLE_EXCEPTION",
487
+ message: `heartbeat_check cycle threw unexpectedly: ${msg.slice(0, 200)}`,
488
+ nextStep: "check_logs_and_report",
489
+ },
490
+ warnings: [],
491
+ sourceRefs: [],
492
+ };
493
+ return envelope;
494
+ }
495
+ }
496
+ if (command === "fallback") {
497
+ const ref = typeof input?.ref === "string" ? input.ref.trim() : "";
498
+ if (!ref) {
499
+ return {
500
+ ok: false,
501
+ error: {
502
+ code: "MISSING_FALLBACK_REF",
503
+ message: "fallback requires args.ref (e.g. fallback:…)",
504
+ requiredUserInput: ["ref"],
505
+ nextStep: "reinvoke_with_ref",
506
+ },
507
+ };
508
+ }
509
+ if (!deps.readModels?.loadFallbackView) {
510
+ return {
511
+ ok: false,
512
+ error: {
513
+ code: "FALLBACK_READ_MODEL_UNAVAILABLE",
514
+ message: "Operator fallback view requires workspace read models",
515
+ requiredUserInput: ["ref"],
516
+ nextStep: "wire_read_models_into_ops_router",
517
+ },
518
+ };
519
+ }
520
+ return (async () => {
521
+ try {
522
+ const data = await showOperatorFallback(ref, deps.readModels);
523
+ return { ok: true, command: "fallback", data };
524
+ }
525
+ catch (error) {
526
+ if (error instanceof OperatorFallbackNotFoundError) {
527
+ return {
528
+ ok: false,
529
+ command: "fallback",
530
+ error: {
531
+ code: error.code,
532
+ message: error.message,
533
+ requiredUserInput: ["ref"],
534
+ nextStep: "verify_fallback_ref_from_delivery_audit",
535
+ },
536
+ };
537
+ }
538
+ throw error;
539
+ }
540
+ })();
541
+ }
542
+ if (command === "capability_probe") {
543
+ // T1.2.8 (SN-CODE-03): run host capability probe with static unknown adapter (CLI context).
544
+ // Persists report when observabilityDb is available; returns safe JSON subset.
545
+ return (async () => {
546
+ const adapter = createStaticUnknownAdapter(deps.workspaceRoot);
547
+ const docCheckedAt = new Date().toISOString();
548
+ const report = probeHostCapability({
549
+ adapter,
550
+ docLinks: [],
551
+ docCheckedAt,
552
+ });
553
+ if (deps.observabilityDb) {
554
+ await recordHostCapability(deps.observabilityDb, report);
555
+ }
556
+ return {
557
+ ok: true,
558
+ command: "capability_probe",
559
+ data: {
560
+ reportId: report.reportId,
561
+ generatedAt: report.generatedAt,
562
+ deliveryTarget: report.deliveryTarget,
563
+ pluginLoad: { verdict: report.pluginLoad.verdict },
564
+ heartbeatBridge: { verdict: report.heartbeatBridge.verdict },
565
+ heartbeatToolInvocation: {
566
+ verdict: report.heartbeatToolInvocation.verdict,
567
+ },
568
+ ackDropBehavior: { verdict: report.ackDropBehavior.verdict },
569
+ conflictCount: report.conflictRecords.length,
570
+ recommendedNextStep: report.recommendedNextStep,
571
+ note: "static_local_probe: all verdicts are unknown without live host context",
572
+ },
573
+ };
574
+ })();
575
+ }
576
+ if (command === "near_real_smoke") {
577
+ // T3.3.2 (SN-CODE-05): wrap runNearRealConnectorSmoke as an ops surface command.
578
+ // Requires state + observabilityDb + workspaceRoot to be wired into OpsRouterDeps.
579
+ if (!deps.state || !deps.observabilityDb || !deps.workspaceRoot) {
580
+ return {
581
+ ok: false,
582
+ command: "near_real_smoke",
583
+ error: {
584
+ code: "NEAR_REAL_SMOKE_DEPS_UNAVAILABLE",
585
+ message: "near_real_smoke requires state, observabilityDb, and workspaceRoot in OpsRouterDeps",
586
+ nextStep: "wire_deps_into_ops_router",
587
+ },
588
+ };
589
+ }
590
+ return (async () => {
591
+ const result = await runNearRealConnectorSmoke({
592
+ state: deps.state,
593
+ observabilityDb: deps.observabilityDb,
594
+ workspaceRoot: deps.workspaceRoot,
595
+ });
596
+ return {
597
+ ok: true,
598
+ command: "near_real_smoke",
599
+ data: result,
600
+ };
601
+ })();
602
+ }
603
+ if (command === "connector_init") {
604
+ // T1.3.1 (SN-CODE-06): generate connector manifest stub.
605
+ return (async () => {
606
+ const result = await connectorInit({
607
+ platformId: typeof input?.platformId === "string" ? input.platformId : "",
608
+ family: typeof input?.family === "string"
609
+ ? input.family
610
+ : undefined,
611
+ displayName: typeof input?.displayName === "string" ? input.displayName : undefined,
612
+ runnerKind: typeof input?.runnerKind === "string"
613
+ ? input.runnerKind
614
+ : undefined,
615
+ force: Boolean(input?.force),
616
+ workspaceRoot: deps.workspaceRoot,
617
+ });
618
+ return result;
619
+ })();
620
+ }
621
+ if (command === "connector_behavior_add") {
622
+ return connectorBehaviorAdd({
623
+ platformId: typeof input?.platformId === "string" ? input.platformId : "",
624
+ behaviorId: typeof input?.behaviorId === "string"
625
+ ? input.behaviorId
626
+ : typeof input?.capabilityId === "string"
627
+ ? input.capabilityId
628
+ : "",
629
+ description: typeof input?.description === "string" ? input.description : undefined,
630
+ channel: typeof input?.channel === "string" ? input.channel : undefined,
631
+ sourceRefs: input?.sourceRefs,
632
+ observedCount: typeof input?.observedCount === "number" ? input.observedCount : undefined,
633
+ workspaceRoot: typeof input?.workspaceRoot === "string"
634
+ ? input.workspaceRoot
635
+ : deps.workspaceRoot,
636
+ });
637
+ }
638
+ if (command === "connector_status") {
639
+ return connectorStatus(deps.registry, undefined, {
640
+ includeHealth: Boolean(input?.includeHealth),
641
+ workspaceRoot: typeof input?.workspaceRoot === "string"
642
+ ? input.workspaceRoot
643
+ : deps.workspaceRoot,
644
+ });
645
+ }
646
+ if (command === "connector_test") {
647
+ // v7 T-V7C.C.1: dryRun=false is the canonical wet probe switch.
648
+ const isWet = input?.wet === true ||
649
+ input?.wet === "true" ||
650
+ input?.dryRun === false ||
651
+ input?.dryRun === "false";
652
+ const result = await connectorTest(deps.registry, {
653
+ platformId: typeof input?.platformId === "string" ? input.platformId : "",
654
+ dryRun: isWet ? false : (input?.dryRun === false ? false : true),
655
+ workspaceRoot: typeof input?.workspaceRoot === "string"
656
+ ? input.workspaceRoot
657
+ : deps.workspaceRoot,
658
+ });
659
+ if (!isWet || !result.ok) {
660
+ return result;
661
+ }
662
+ const data = result.data && typeof result.data === "object"
663
+ ? result.data
664
+ : {};
665
+ const capabilities = Array.isArray(data.capabilities)
666
+ ? data.capabilities.filter((item) => typeof item === "string")
667
+ : [];
668
+ const capabilityId = textInput(input, "capabilityId") ?? capabilities[0] ?? "";
669
+ if (!capabilityId) {
670
+ return {
671
+ ok: false,
672
+ command: "connector_test",
673
+ error: {
674
+ code: "MISSING_CAPABILITY_ID",
675
+ message: "wet connector_test requires capabilityId or at least one connector capability",
676
+ requiredUserInput: ["capabilityId"],
677
+ nextStep: "reinvoke_with_capability_id",
678
+ },
679
+ };
680
+ }
681
+ const platformId = String(data.platformId ?? input?.platformId ?? "");
682
+ const registryEntry = deps.registry?.describeConnector(platformId);
683
+ if (!registryEntry) {
684
+ return result;
685
+ }
686
+ const registryV7 = new CapabilityContractRegistryV7();
687
+ registerConnectorForWetProbe({
688
+ registryV7,
689
+ entry: {
690
+ platformId: registryEntry.platformId,
691
+ capabilities: registryEntry.capabilities,
692
+ manifestPath: registryEntry.manifestPath,
693
+ },
694
+ workspaceRoot: typeof input?.workspaceRoot === "string"
695
+ ? input.workspaceRoot
696
+ : deps.workspaceRoot,
697
+ selectedCapabilityId: capabilityId,
698
+ safeEndpoint: textInput(input, "safeEndpoint"),
699
+ });
700
+ const wetResult = await createWetProbeRunner().runWetProbe(platformId, capabilityId, registryV7);
701
+ const warnings = [];
702
+ let persistedProbeResult = false;
703
+ if (deps.state) {
704
+ await createCapabilityProbeResultStore(deps.state).appendProbeResult(wetResult.probeResult);
705
+ persistedProbeResult = true;
706
+ }
707
+ else {
708
+ warnings.push("state_db_unavailable:capability_probe_result_not_persisted");
709
+ }
710
+ return {
711
+ // T-V7C.C.5: only "available" (HTTP 200-299) counts as success;
712
+ // "degraded" (429/503) and "unavailable" both result in ok=false.
713
+ ok: wetResult.probeResult.actualStatus === "available",
714
+ command: "connector_test",
715
+ data: {
716
+ ...data,
717
+ dryRun: false,
718
+ capabilityId,
719
+ actualStatus: wetResult.probeResult.actualStatus,
720
+ httpStatus: wetResult.probeResult.httpStatus ?? wetResult.httpStatus,
721
+ probeResultId: wetResult.probeResult.probeResultId,
722
+ probeConfigRef: wetResult.probeResult.probeConfigRef,
723
+ sampleResponseRef: wetResult.probeResult.sampleResponseRef,
724
+ persistedProbeResult,
725
+ triggerSource: "manual_run",
726
+ affectsHeartbeatCadence: false,
727
+ note: "wet probe mode: executed safe probe endpoint and persisted capability_probe_result when state DB is available",
728
+ },
729
+ warnings,
730
+ };
731
+ }
732
+ if (command === "connector:run") {
733
+ // T-ROS.C.3: manual connector execution isolated from heartbeat cadence
734
+ const platformId = typeof input?.platformId === "string" ? input.platformId : "";
735
+ const capabilityId = typeof input?.capabilityId === "string" ? input.capabilityId : "";
736
+ if (!platformId || !capabilityId) {
737
+ return {
738
+ ok: false,
739
+ command: "connector:run",
740
+ error: {
741
+ code: "MISSING_PLATFORM_OR_CAPABILITY_ID",
742
+ message: "connector:run requires platformId and capabilityId",
743
+ requiredUserInput: ["platformId", "capabilityId"],
744
+ nextStep: "reinvoke_with_platform_and_capability_id",
745
+ },
746
+ };
747
+ }
748
+ if (!deps.connectorExecutor || !deps.state) {
749
+ return {
750
+ ok: false,
751
+ command: "connector:run",
752
+ error: {
753
+ code: "MANUAL_RUN_DEPS_UNAVAILABLE",
754
+ message: "connector:run requires connectorExecutor and state database",
755
+ nextStep: "wire_connector_executor_and_state_into_ops_router",
756
+ },
757
+ };
758
+ }
759
+ const toolExperienceStore = createToolExperienceStore(deps.state);
760
+ const experienceWriter = createExperienceWriter(toolExperienceStore);
761
+ const wetProbeRunner = createWetProbeRunner();
762
+ const registryV7 = new CapabilityContractRegistryV7();
763
+ // Populate V7 registry from dynamic registry if available (best-effort)
764
+ if (deps.registry) {
765
+ for (const entry of deps.registry.listConnectors()) {
766
+ if (entry.manifestPath) {
767
+ try {
768
+ const manifestText = fs.readFileSync(entry.manifestPath, "utf-8");
769
+ const manifest = JSON.parse(manifestText);
770
+ registryV7.register(manifest);
771
+ }
772
+ catch {
773
+ // Skip manifests that can't be read or don't validate as V7
774
+ }
775
+ }
776
+ }
777
+ }
778
+ const dispatcher = createManualRunDispatcher({
779
+ connectorExecutor: deps.connectorExecutor,
780
+ experienceWriter,
781
+ wetProbeRunner,
782
+ registryV7,
783
+ });
784
+ return dispatcher.runConnector({
785
+ platformId,
786
+ capabilityId,
787
+ payload: typeof input?.payload === "object" && input?.payload !== null
788
+ ? input.payload
789
+ : undefined,
790
+ caller: typeof input?.caller === "string" ? input.caller : undefined,
791
+ reason: typeof input?.reason === "string" ? input.reason : undefined,
792
+ });
793
+ }
794
+ if (command === "goal") {
795
+ const rawAction = typeof input?.action === "string" ? input.action : "list";
796
+ const action = ["set", "list", "accept", "reject"].includes(rawAction)
797
+ ? rawAction
798
+ : "list";
799
+ const sanitizeText = (v, maxLen = 1000) => {
800
+ if (typeof v !== "string")
801
+ return undefined;
802
+ const trimmed = v.trim();
803
+ if (trimmed.length === 0)
804
+ return undefined;
805
+ return trimmed.slice(0, maxLen);
806
+ };
807
+ return goalCommand(deps.state, {
808
+ action,
809
+ goalId: typeof input?.goalId === "string" ? input.goalId.trim().slice(0, 128) : undefined,
810
+ description: sanitizeText(input?.description),
811
+ completionCriteria: sanitizeText(input?.completionCriteria),
812
+ // T1.4.2: criteria alias for completionCriteria
813
+ criteria: sanitizeText(input?.criteria),
814
+ risk: typeof input?.risk === "string"
815
+ ? input.risk
816
+ : undefined,
817
+ kind: typeof input?.kind === "string"
818
+ ? input.kind
819
+ : undefined,
820
+ statusFilter: typeof input?.statusFilter === "string" ? input.statusFilter : undefined,
821
+ originFilter: typeof input?.originFilter === "string" ? input.originFilter : undefined,
822
+ limit: typeof input?.limit === "number" ? input.limit : undefined,
823
+ });
824
+ }
825
+ if (command === "dream:recent") {
826
+ if (!deps.readModels) {
827
+ return {
828
+ ok: false,
829
+ error: {
830
+ code: "READ_MODELS_UNAVAILABLE",
831
+ message: "dream:recent requires workspace read models",
832
+ nextStep: "wire_read_models_into_ops_router",
833
+ },
834
+ };
835
+ }
836
+ const limit = typeof input?.limit === "number" ? input.limit : 5;
837
+ const data = await deps.readModels.loadDreamRecent(limit);
838
+ return { ok: true, data };
839
+ }
840
+ if (command === "cycle:recent") {
841
+ if (!deps.readModels) {
842
+ return {
843
+ ok: false,
844
+ error: {
845
+ code: "READ_MODELS_UNAVAILABLE",
846
+ message: "cycle:recent requires workspace read models",
847
+ nextStep: "wire_read_models_into_ops_router",
848
+ },
849
+ };
850
+ }
851
+ const limit = typeof input?.limit === "number" ? input.limit : 5;
852
+ const data = await deps.readModels.loadCycleRecent(limit);
853
+ return { ok: true, data };
854
+ }
855
+ // ─── v8 commands (T-ROS.C.1) ─────────────────────────────────────────
856
+ /**
857
+ * [G1] loop_status — v8 causal loop health read model.
858
+ * Returns machine-readable overallStatus, stalledAt, stageSummaries,
859
+ * and human-readable nextAction for operator diagnosis.
860
+ */
861
+ if (command === "loop_status") {
862
+ const generatedAt = new Date().toISOString();
863
+ if (!deps.state) {
864
+ const envelope = {
865
+ ok: false,
866
+ command: "loop_status",
867
+ runtimeMode: "unavailable",
868
+ surfaceMode: "cli",
869
+ generatedAt,
870
+ error: {
871
+ code: "STATE_DB_UNAVAILABLE",
872
+ message: "loop_status requires state database in OpsRouterDeps",
873
+ nextStep: "wire_state_db_into_ops_router",
874
+ },
875
+ warnings: [],
876
+ sourceRefs: [],
877
+ };
878
+ return envelope;
879
+ }
880
+ try {
881
+ const result = await readLoopStatus(deps.state);
882
+ if (!result.ok) {
883
+ const envelope = {
884
+ ok: false,
885
+ command: "loop_status",
886
+ runtimeMode: "workspace_full_runtime",
887
+ surfaceMode: "cli",
888
+ generatedAt,
889
+ error: {
890
+ code: "LOOP_STATUS_DEGRADED",
891
+ message: result.degraded.operatorNextAction,
892
+ nextStep: "check_state_db_and_retry",
893
+ },
894
+ warnings: [result.degraded.reason],
895
+ sourceRefs: result.degraded.sourceRefs.map((r) => r.uri),
896
+ };
897
+ return envelope;
898
+ }
899
+ const envelope = {
900
+ ok: true,
901
+ command: "loop_status",
902
+ runtimeMode: "workspace_full_runtime",
903
+ surfaceMode: "cli",
904
+ generatedAt,
905
+ data: result.status,
906
+ warnings: [],
907
+ sourceRefs: [],
908
+ };
909
+ return envelope;
910
+ }
911
+ catch (err) {
912
+ const msg = err instanceof Error ? err.message : String(err);
913
+ const envelope = {
914
+ ok: false,
915
+ command: "loop_status",
916
+ runtimeMode: "unavailable",
917
+ surfaceMode: "cli",
918
+ generatedAt,
919
+ error: { code: "LOOP_STATUS_EXCEPTION", message: msg },
920
+ warnings: [],
921
+ sourceRefs: [],
922
+ };
923
+ return envelope;
924
+ }
925
+ }
926
+ // ─── v7 commands (T-ROS.C.1) ─────────────────────────────────────────
927
+ /** [G2] self_health — transparent pass-through from SelfHealthSnapshot (DR-042). */
928
+ if (command === "self_health") {
929
+ const generatedAt = new Date().toISOString();
930
+ try {
931
+ ensureMinimumProbes();
932
+ const snap = await getSelfHealthSnapshot();
933
+ const degraded_dimensions = Object.entries(snap.dimensions)
934
+ .filter(([, d]) => d.status === "degraded")
935
+ .map(([k]) => k);
936
+ const envelope = {
937
+ ok: true,
938
+ command: "self_health",
939
+ runtimeMode: "workspace_full_runtime",
940
+ surfaceMode: "cli",
941
+ generatedAt,
942
+ data: {
943
+ overall: snap.overall,
944
+ generatedAt: snap.generatedAt,
945
+ degraded_dimensions,
946
+ dimensions: snap.dimensions,
947
+ },
948
+ warnings: [],
949
+ sourceRefs: ["observability/services/self-health-snapshot.ts"],
950
+ };
951
+ return envelope;
952
+ }
953
+ catch (err) {
954
+ const msg = err instanceof Error ? err.message : String(err);
955
+ const envelope = {
956
+ ok: false,
957
+ command: "self_health",
958
+ runtimeMode: "unavailable",
959
+ surfaceMode: "cli",
960
+ generatedAt,
961
+ error: { code: "SELF_HEALTH_PROBE_FAILED", message: msg },
962
+ warnings: [],
963
+ sourceRefs: [],
964
+ };
965
+ return envelope;
966
+ }
967
+ }
968
+ /**
969
+ * [G3] tool_affordance — body-tool AffordanceMap pass-through.
970
+ * Port not yet wired in this wave; returns degraded view with clear next-step.
971
+ */
972
+ if (command === "tool_affordance") {
973
+ const generatedAt = new Date().toISOString();
974
+ if (deps.toolAffordancePort) {
975
+ const allStatuses = [
976
+ "safe",
977
+ "exploratory",
978
+ "needs_auth",
979
+ "painful",
980
+ "unavailable",
981
+ ];
982
+ const platformIds = Array.isArray(input?.platformIds)
983
+ ? input.platformIds.filter((item) => typeof item === "string")
984
+ : typeof input?.platformId === "string"
985
+ ? [input.platformId]
986
+ : undefined;
987
+ const data = await deps.toolAffordancePort.assembleAffordanceMap({
988
+ platformIds,
989
+ allowedStatuses: allStatuses,
990
+ goalKind: typeof input?.goalKind === "string" ? input.goalKind : undefined,
991
+ });
992
+ const envelope = {
993
+ ok: true,
994
+ command: "tool_affordance",
995
+ runtimeMode: "workspace_full_runtime",
996
+ surfaceMode: "cli",
997
+ generatedAt,
998
+ data,
999
+ warnings: [],
1000
+ sourceRefs: [
1001
+ "core/second-nature/body/tool-affordance/affordance-assembler.ts",
1002
+ ],
1003
+ };
1004
+ return envelope;
1005
+ }
1006
+ const envelope = {
1007
+ ok: false,
1008
+ command: "tool_affordance",
1009
+ runtimeMode: "unavailable",
1010
+ surfaceMode: "cli",
1011
+ generatedAt,
1012
+ error: {
1013
+ code: "TOOL_AFFORDANCE_PORT_UNWIRED",
1014
+ message: "tool_affordance requires body-tool AffordanceMap port (T-BTS.C.1) to be wired into OpsRouterDeps",
1015
+ nextStep: "wire_body_tool_port_into_ops_router_deps",
1016
+ },
1017
+ warnings: [],
1018
+ sourceRefs: [],
1019
+ };
1020
+ return envelope;
1021
+ }
1022
+ /**
1023
+ * [G6] heartbeat_digest — wraps generateHeartbeatDigest.
1024
+ * Requires auditStore in deps; degrades if unavailable.
1025
+ */
1026
+ if (command === "heartbeat_digest") {
1027
+ const generatedAt = new Date().toISOString();
1028
+ if (!deps.auditStore) {
1029
+ const envelope = {
1030
+ ok: false,
1031
+ command: "heartbeat_digest",
1032
+ runtimeMode: "unavailable",
1033
+ surfaceMode: "cli",
1034
+ generatedAt,
1035
+ error: {
1036
+ code: "AUDIT_STORE_UNAVAILABLE",
1037
+ message: "heartbeat_digest requires auditStore in OpsRouterDeps",
1038
+ nextStep: "wire_audit_store_into_ops_router",
1039
+ },
1040
+ warnings: [],
1041
+ sourceRefs: [],
1042
+ };
1043
+ return envelope;
1044
+ }
1045
+ const date = typeof input?.date === "string" && input.date
1046
+ ? input.date
1047
+ : new Date().toISOString().slice(0, 10);
1048
+ try {
1049
+ const digestDeps = {
1050
+ auditStore: deps.auditStore,
1051
+ ...deps.heartbeatDigestDeps,
1052
+ };
1053
+ const digest = await generateHeartbeatDigest(date, digestDeps);
1054
+ const envelope = {
1055
+ ok: true,
1056
+ command: "heartbeat_digest",
1057
+ runtimeMode: "workspace_full_runtime",
1058
+ surfaceMode: "cli",
1059
+ generatedAt,
1060
+ data: digest,
1061
+ warnings: [],
1062
+ sourceRefs: ["observability/services/heartbeat-digest-assembler.ts"],
1063
+ };
1064
+ return envelope;
1065
+ }
1066
+ catch (err) {
1067
+ const msg = err instanceof Error ? err.message : String(err);
1068
+ const envelope = {
1069
+ ok: false,
1070
+ command: "heartbeat_digest",
1071
+ runtimeMode: "unavailable",
1072
+ surfaceMode: "cli",
1073
+ generatedAt,
1074
+ error: { code: "DIGEST_GENERATION_FAILED", message: msg },
1075
+ warnings: [],
1076
+ sourceRefs: [],
1077
+ };
1078
+ return envelope;
1079
+ }
1080
+ }
1081
+ /**
1082
+ * [G6] snapshot:capture — production capture path for RestoreSnapshot +
1083
+ * NarrativeTimeline. This gives restore and narrative:diff real state to consume.
1084
+ */
1085
+ if (command === "snapshot:capture") {
1086
+ return captureRuntimeSnapshot(deps, input);
1087
+ }
1088
+ /**
1089
+ * [G6] narrative:diff queryNarrativeDiff between two versions.
1090
+ * Requires narrativeTimelineDeps in OpsRouterDeps.
1091
+ */
1092
+ if (command === "narrative:diff") {
1093
+ const generatedAt = new Date().toISOString();
1094
+ if (!deps.narrativeTimelineDeps) {
1095
+ const envelope = {
1096
+ ok: false,
1097
+ command: "narrative:diff",
1098
+ runtimeMode: "unavailable",
1099
+ surfaceMode: "cli",
1100
+ generatedAt,
1101
+ error: {
1102
+ code: "NARRATIVE_TIMELINE_PORT_UNAVAILABLE",
1103
+ message: "narrative:diff requires narrativeTimelineDeps in OpsRouterDeps",
1104
+ nextStep: "wire_narrative_timeline_deps_into_ops_router",
1105
+ },
1106
+ warnings: [],
1107
+ sourceRefs: [],
1108
+ };
1109
+ return envelope;
1110
+ }
1111
+ const fromVersion = typeof input?.from === "string" ? input.from : "";
1112
+ const toVersion = typeof input?.to === "string" ? input.to : "";
1113
+ if (!fromVersion || !toVersion) {
1114
+ const envelope = {
1115
+ ok: false,
1116
+ command: "narrative:diff",
1117
+ runtimeMode: "workspace_full_runtime",
1118
+ surfaceMode: "cli",
1119
+ generatedAt,
1120
+ error: {
1121
+ code: "MISSING_VERSIONS",
1122
+ message: "narrative:diff requires 'from' and 'to' version arguments",
1123
+ nextStep: "reinvoke_with_from_and_to",
1124
+ },
1125
+ warnings: [],
1126
+ sourceRefs: [],
1127
+ };
1128
+ return envelope;
1129
+ }
1130
+ try {
1131
+ const diff = await queryNarrativeDiff(fromVersion, toVersion, deps.narrativeTimelineDeps);
1132
+ const envelope = {
1133
+ ok: true,
1134
+ command: "narrative:diff",
1135
+ runtimeMode: "workspace_full_runtime",
1136
+ surfaceMode: "cli",
1137
+ generatedAt,
1138
+ data: diff,
1139
+ warnings: [],
1140
+ sourceRefs: ["observability/services/narrative-timeline-query-service.ts"],
1141
+ };
1142
+ return envelope;
1143
+ }
1144
+ catch (err) {
1145
+ if (err instanceof NarrativeVersionNotFoundError) {
1146
+ const envelope = {
1147
+ ok: false,
1148
+ command: "narrative:diff",
1149
+ runtimeMode: "workspace_full_runtime",
1150
+ surfaceMode: "cli",
1151
+ generatedAt,
1152
+ error: {
1153
+ code: "NARRATIVE_VERSION_NOT_FOUND",
1154
+ message: err.message,
1155
+ nextStep: "verify_version_exists_in_timeline",
1156
+ },
1157
+ warnings: [],
1158
+ sourceRefs: [],
1159
+ };
1160
+ return envelope;
1161
+ }
1162
+ const msg = err instanceof Error ? err.message : String(err);
1163
+ const envelope = {
1164
+ ok: false,
1165
+ command: "narrative:diff",
1166
+ runtimeMode: "unavailable",
1167
+ surfaceMode: "cli",
1168
+ generatedAt,
1169
+ error: { code: "NARRATIVE_DIFF_FAILED", message: msg },
1170
+ warnings: [],
1171
+ sourceRefs: [],
1172
+ };
1173
+ return envelope;
1174
+ }
1175
+ }
1176
+ /**
1177
+ * [G6] timeline — queryNarrativeTimeline with cursor pagination.
1178
+ * Requires narrativeTimelineDeps in OpsRouterDeps.
1179
+ */
1180
+ if (command === "timeline") {
1181
+ const generatedAt = new Date().toISOString();
1182
+ if (!deps.narrativeTimelineDeps) {
1183
+ const envelope = {
1184
+ ok: false,
1185
+ command: "timeline",
1186
+ runtimeMode: "unavailable",
1187
+ surfaceMode: "cli",
1188
+ generatedAt,
1189
+ error: {
1190
+ code: "NARRATIVE_TIMELINE_PORT_UNAVAILABLE",
1191
+ message: "timeline requires narrativeTimelineDeps in OpsRouterDeps",
1192
+ nextStep: "wire_narrative_timeline_deps_into_ops_router",
1193
+ },
1194
+ warnings: [],
1195
+ sourceRefs: [],
1196
+ };
1197
+ return envelope;
1198
+ }
1199
+ const now = new Date();
1200
+ const to = typeof input?.to === "string" ? input.to : now.toISOString();
1201
+ const from = typeof input?.from === "string"
1202
+ ? input.from
1203
+ : new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000).toISOString();
1204
+ const limit = typeof input?.limit === "number" ? input.limit : 20;
1205
+ const cursor = typeof input?.cursor === "string" ? input.cursor : undefined;
1206
+ try {
1207
+ const page = await queryNarrativeTimeline(from, to, { limit, cursor }, deps.narrativeTimelineDeps);
1208
+ const envelope = {
1209
+ ok: true,
1210
+ command: "timeline",
1211
+ runtimeMode: "workspace_full_runtime",
1212
+ surfaceMode: "cli",
1213
+ generatedAt,
1214
+ data: page,
1215
+ warnings: [],
1216
+ sourceRefs: ["observability/services/narrative-timeline-query-service.ts"],
1217
+ };
1218
+ return envelope;
1219
+ }
1220
+ catch (err) {
1221
+ const msg = err instanceof Error ? err.message : String(err);
1222
+ const code = err.name === "NarrativeQueryRangeError"
1223
+ ? "NARRATIVE_RANGE_EXCEEDED"
1224
+ : "TIMELINE_QUERY_FAILED";
1225
+ const envelope = {
1226
+ ok: false,
1227
+ command: "timeline",
1228
+ runtimeMode: "unavailable",
1229
+ surfaceMode: "cli",
1230
+ generatedAt,
1231
+ error: { code, message: msg },
1232
+ warnings: [],
1233
+ sourceRefs: [],
1234
+ };
1235
+ return envelope;
1236
+ }
1237
+ }
1238
+ /**
1239
+ * [G6] restore — bounded state restoration via RestoreSnapshotStore + audit (T-ROS.C.1, T-OBS.C.6).
1240
+ * When restoreSnapshotStore is wired, attempts to apply the snapshot payload back to state.
1241
+ * Always writes RestoreAudit. Never restores credential fields.
1242
+ */
1243
+ if (command === "restore") {
1244
+ const generatedAt = new Date().toISOString();
1245
+ if (!deps.auditStore) {
1246
+ const envelope = {
1247
+ ok: false,
1248
+ command: "restore",
1249
+ runtimeMode: "unavailable",
1250
+ surfaceMode: "cli",
1251
+ generatedAt,
1252
+ error: {
1253
+ code: "AUDIT_STORE_UNAVAILABLE",
1254
+ message: "restore requires auditStore in OpsRouterDeps",
1255
+ nextStep: "wire_audit_store_into_ops_router",
1256
+ },
1257
+ warnings: [],
1258
+ sourceRefs: [],
1259
+ };
1260
+ return envelope;
1261
+ }
1262
+ let restoreTarget;
1263
+ let fromVersion;
1264
+ let toVersion;
1265
+ // T-V7C.C.5: snapshotId operator-friendly parameter takes precedence over legacy fields.
1266
+ // When snapshotId is provided, resolve restoreTarget/fromVersion/toVersion from the
1267
+ // matching snapshot row; otherwise fall back to explicit legacy parameters.
1268
+ const snapshotId = textInput(input, "snapshotId");
1269
+ if (snapshotId) {
1270
+ if (!deps.restoreSnapshotStore) {
1271
+ const envelope = {
1272
+ ok: false,
1273
+ command: "restore",
1274
+ runtimeMode: "unavailable",
1275
+ surfaceMode: "cli",
1276
+ generatedAt,
1277
+ error: {
1278
+ code: "RESTORE_SNAPSHOT_STORE_UNAVAILABLE",
1279
+ message: "snapshotId restore requires restoreSnapshotStore in OpsRouterDeps",
1280
+ nextStep: "wire_restore_snapshot_store_into_ops_router",
1281
+ },
1282
+ warnings: [],
1283
+ sourceRefs: [],
1284
+ };
1285
+ return envelope;
1286
+ }
1287
+ const snapshots = await deps.restoreSnapshotStore.listSnapshots();
1288
+ const match = snapshots.find((s) => s.snapshotId === snapshotId);
1289
+ if (match) {
1290
+ restoreTarget = snapshotId;
1291
+ fromVersion = match.capturedAt;
1292
+ toVersion = snapshotId;
1293
+ }
1294
+ else {
1295
+ const envelope = {
1296
+ ok: false,
1297
+ command: "restore",
1298
+ runtimeMode: "workspace_full_runtime",
1299
+ surfaceMode: "cli",
1300
+ generatedAt,
1301
+ error: {
1302
+ code: "SNAPSHOT_NOT_FOUND",
1303
+ message: `snapshotId ${snapshotId} not found in restore_snapshot table`,
1304
+ nextStep: "list_available_snapshots_or_verify_snapshotId",
1305
+ },
1306
+ warnings: [],
1307
+ sourceRefs: [],
1308
+ };
1309
+ return envelope;
1310
+ }
1311
+ }
1312
+ else {
1313
+ const missingFields = [];
1314
+ if (typeof input?.restoreTarget !== "string")
1315
+ missingFields.push("restoreTarget");
1316
+ if (typeof input?.fromVersion !== "string")
1317
+ missingFields.push("fromVersion");
1318
+ if (typeof input?.toVersion !== "string")
1319
+ missingFields.push("toVersion");
1320
+ if (missingFields.length > 0) {
1321
+ const envelope = {
1322
+ ok: false,
1323
+ command: "restore",
1324
+ runtimeMode: "workspace_full_runtime",
1325
+ surfaceMode: "cli",
1326
+ generatedAt,
1327
+ error: {
1328
+ code: "MISSING_RESTORE_FIELDS",
1329
+ message: `restore requires: ${missingFields.join(", ")}`,
1330
+ nextStep: "reinvoke_with_required_fields",
1331
+ },
1332
+ warnings: [],
1333
+ sourceRefs: [],
1334
+ };
1335
+ return envelope;
1336
+ }
1337
+ restoreTarget = input.restoreTarget;
1338
+ fromVersion = input.fromVersion;
1339
+ toVersion = input.toVersion;
1340
+ }
1341
+ // [NEW] Invoke bounded restore via RestoreSnapshotStore when wired
1342
+ let restoreResult = {
1343
+ ok: false,
1344
+ completedEntities: [],
1345
+ failedEntities: [],
1346
+ warnings: ["restore_snapshot_store_unavailable"],
1347
+ };
1348
+ if (deps.restoreSnapshotStore) {
1349
+ restoreResult = await deps.restoreSnapshotStore.applyBoundedRestore({
1350
+ restoreTarget: restoreTarget,
1351
+ fromVersion: fromVersion,
1352
+ toVersion: toVersion,
1353
+ });
1354
+ }
1355
+ const event = {
1356
+ id: `restore-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
1357
+ restoreTarget: restoreTarget,
1358
+ fromVersion: fromVersion,
1359
+ toVersion: toVersion,
1360
+ triggeredBy: input?.triggeredBy ?? "operator",
1361
+ reason: typeof input?.reason === "string" ? input.reason : "manual_restore",
1362
+ completedEntities: restoreResult.completedEntities,
1363
+ failedEntities: restoreResult.failedEntities,
1364
+ // credentials are always excluded from restore audit
1365
+ excludedFields: Array.isArray(input?.excludedFields)
1366
+ ? input.excludedFields.filter((f) => typeof f === "string")
1367
+ : ["credential", "encryptionKey"],
1368
+ restoredFieldCount: restoreResult.completedEntities.length,
1369
+ createdAt: generatedAt,
1370
+ traceId: typeof input?.traceId === "string" ? input.traceId : `trace-restore-${Date.now()}`,
1371
+ };
1372
+ const auditResult = await writeRestoreAudit(event, deps.auditStore);
1373
+ const envelope = {
1374
+ ok: restoreResult.ok && auditResult.ok,
1375
+ command: "restore",
1376
+ runtimeMode: "workspace_full_runtime",
1377
+ surfaceMode: "cli",
1378
+ generatedAt,
1379
+ data: {
1380
+ auditWritten: auditResult.warnings.length === 0,
1381
+ fromVersion: event.fromVersion,
1382
+ toVersion: event.toVersion,
1383
+ restoreTarget: event.restoreTarget,
1384
+ isPartialRestore: event.failedEntities.length > 0,
1385
+ failedEntities: event.failedEntities,
1386
+ completedEntities: event.completedEntities,
1387
+ restoreSnapshotStoreAvailable: !!deps.restoreSnapshotStore,
1388
+ },
1389
+ warnings: [...restoreResult.warnings, ...auditResult.warnings],
1390
+ sourceRefs: [
1391
+ "observability/services/restore-audit-service.ts",
1392
+ "storage/services/restore-snapshot-store.ts",
1393
+ ],
1394
+ };
1395
+ return envelope;
1396
+ }
1397
+ /**
1398
+ * [G7] runtime_secret_bootstrap — RuntimeSecretAnchorView pass-through.
1399
+ * Requires secretAnchorDeps in OpsRouterDeps; never returns key plaintext.
1400
+ */
1401
+ if (command === "runtime_secret_bootstrap") {
1402
+ const generatedAt = new Date().toISOString();
1403
+ if (!deps.secretAnchorDeps) {
1404
+ const envelope = {
1405
+ ok: false,
1406
+ command: "runtime_secret_bootstrap",
1407
+ runtimeMode: "unavailable",
1408
+ surfaceMode: "cli",
1409
+ generatedAt,
1410
+ error: {
1411
+ code: "SECRET_ANCHOR_DEPS_UNAVAILABLE",
1412
+ message: "runtime_secret_bootstrap requires secretAnchorDeps in OpsRouterDeps",
1413
+ nextStep: "wire_secret_anchor_deps_into_ops_router",
1414
+ },
1415
+ warnings: [],
1416
+ sourceRefs: [],
1417
+ };
1418
+ return envelope;
1419
+ }
1420
+ try {
1421
+ const view = await viewSecretAnchor(deps.secretAnchorDeps);
1422
+ // Map to RuntimeSecretBootstrapView (design model §6.1)
1423
+ const data = {
1424
+ status: view.status === "verified" || view.status === "ok"
1425
+ ? "ok"
1426
+ : view.status === "missing"
1427
+ ? "runtime_secret_anchor_missing"
1428
+ : view.status === "wrong_key"
1429
+ ? "credential_recovery_required"
1430
+ : view.status === "decryption_failed"
1431
+ ? "runtime_secret_unavailable"
1432
+ : "unknown",
1433
+ keyHealth: view.status === "verified" || view.status === "ok"
1434
+ ? "ok"
1435
+ : view.status === "missing"
1436
+ ? "missing_key"
1437
+ : view.status === "wrong_key"
1438
+ ? "wrong_key"
1439
+ : "unknown",
1440
+ anchorLocation: view.keyPath,
1441
+ recoveryPrincipleRef: view.recoveryDocRef,
1442
+ plaintextKeyExposed: false,
1443
+ reasonCode: view.reasonCode,
1444
+ recoverySteps: view.recoverySteps,
1445
+ };
1446
+ const envelope = {
1447
+ ok: true,
1448
+ command: "runtime_secret_bootstrap",
1449
+ runtimeMode: "workspace_full_runtime",
1450
+ surfaceMode: "cli",
1451
+ generatedAt,
1452
+ data,
1453
+ warnings: [],
1454
+ sourceRefs: ["observability/services/runtime-secret-anchor-view.ts"],
1455
+ };
1456
+ return envelope;
1457
+ }
1458
+ catch (err) {
1459
+ const msg = err instanceof Error ? err.message : String(err);
1460
+ const envelope = {
1461
+ ok: false,
1462
+ command: "runtime_secret_bootstrap",
1463
+ runtimeMode: "unavailable",
1464
+ surfaceMode: "cli",
1465
+ generatedAt,
1466
+ error: { code: "SECRET_ANCHOR_PROBE_FAILED", message: msg },
1467
+ warnings: [],
1468
+ sourceRefs: [],
1469
+ };
1470
+ return envelope;
1471
+ }
1472
+ }
1473
+ // ─── T-V7C.C.4R: guidance_payload ──────────────────────────────────────
1474
+ // Returns the assembled impulse + atmosphere for a given scene context.
1475
+ // Useful for Claw to inspect what guidance content would be injected before
1476
+ // a real heartbeat cycle, and to verify platform-specific impulse overrides.
1477
+ if (command === "guidance_payload") {
1478
+ const generatedAt = new Date().toISOString();
1479
+ const { assembleImpulseSync } = await import("../../guidance/impulse-assembler.js");
1480
+ const { getBaselineAtmosphereTemplate } = await import("../../guidance/template-registry.js");
1481
+ const sceneType = input?.sceneType ?? "social";
1482
+ const capabilityIntent = typeof input?.capabilityIntent === "string"
1483
+ ? input.capabilityIntent
1484
+ : undefined;
1485
+ const platformId = typeof input?.platformId === "string"
1486
+ ? input.platformId
1487
+ : undefined;
1488
+ const validSceneTypes = ["social", "reply", "outreach", "quiet", "explain", "user_reply"];
1489
+ if (!validSceneTypes.includes(sceneType)) {
1490
+ const envelope = {
1491
+ ok: false,
1492
+ command: "guidance_payload",
1493
+ runtimeMode: "unavailable",
1494
+ surfaceMode: "cli",
1495
+ generatedAt,
1496
+ error: {
1497
+ code: "INVALID_SCENE_TYPE",
1498
+ message: `sceneType must be one of: ${validSceneTypes.join(", ")}`,
1499
+ nextStep: "reinvoke_with_valid_scene_type",
1500
+ },
1501
+ warnings: [],
1502
+ sourceRefs: [],
1503
+ };
1504
+ return envelope;
1505
+ }
1506
+ const impulseResult = assembleImpulseSync({
1507
+ sceneType: sceneType,
1508
+ capabilityIntent,
1509
+ platformId,
1510
+ });
1511
+ const { buildExpressionBoundary } = await import("../../guidance/output-guard.js");
1512
+ const { getShortAtmosphereTemplate } = await import("../../guidance/template-registry.js");
1513
+ const atmosphere = getShortAtmosphereTemplate("active", "low");
1514
+ const expressionBoundary = buildExpressionBoundary(sceneType);
1515
+ const envelope = {
1516
+ ok: true,
1517
+ command: "guidance_payload",
1518
+ runtimeMode: deps.runtimeAvailable ? "workspace_full_runtime" : "host_safe_carrier",
1519
+ surfaceMode: "cli",
1520
+ generatedAt,
1521
+ data: {
1522
+ sceneType,
1523
+ capabilityIntent: capabilityIntent ?? null,
1524
+ platformId: platformId ?? null,
1525
+ capabilityClass: impulseResult.capabilityClass,
1526
+ impulseSource: impulseResult.source,
1527
+ impulseText: impulseResult.impulse?.text ?? null,
1528
+ impulseReviewStatus: impulseResult.impulse?.reviewStatus ?? null,
1529
+ atmosphereText: atmosphere.text,
1530
+ atmosphereReviewStatus: atmosphere.reviewStatus,
1531
+ expressionBoundaryConstraints: expressionBoundary.constraints,
1532
+ expressionBoundaryStyle: expressionBoundary.style,
1533
+ },
1534
+ warnings: impulseResult.source === "none"
1535
+ ? ["no_impulse_available_for_this_scene_and_capability"]
1536
+ : [],
1537
+ sourceRefs: [
1538
+ "guidance/capability-class.ts",
1539
+ "guidance/impulse-assembler.ts",
1540
+ "guidance/template-registry.ts",
1541
+ "guidance/output-guard.ts",
1542
+ ],
1543
+ };
1544
+ return envelope;
1545
+ }
1546
+ return {
1547
+ ok: false,
1548
+ error: {
1549
+ code: "unknown_ops_command",
1550
+ message: `Unknown ops command: ${command}`,
1551
+ },
1552
+ };
1553
+ },
1554
+ };
1555
+ }