@haaaiawd/second-nature 0.2.12 → 0.2.13

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 (59) hide show
  1. package/index.js +96 -6
  2. package/openclaw.plugin.json +1 -1
  3. package/package.json +1 -1
  4. package/runtime/cli/commands/index.js +85 -11
  5. package/runtime/cli/host-capability/host-discovery-port.d.ts +85 -0
  6. package/runtime/cli/host-capability/host-discovery-port.js +137 -0
  7. package/runtime/cli/ops/heartbeat-surface.d.ts +3 -3
  8. package/runtime/cli/ops/heartbeat-surface.js +6 -5
  9. package/runtime/cli/ops/ops-router.d.ts +6 -2
  10. package/runtime/cli/ops/ops-router.js +1273 -1145
  11. package/runtime/connectors/base/normalized-evidence-content.d.ts +4 -0
  12. package/runtime/connectors/base/normalized-evidence-content.js +21 -2
  13. package/runtime/connectors/evidence-normalizer.js +32 -1
  14. package/runtime/core/second-nature/action/action-closure-recorder.d.ts +2 -0
  15. package/runtime/core/second-nature/action/action-closure-recorder.js +49 -34
  16. package/runtime/core/second-nature/action/action-proposal-builder.js +3 -2
  17. package/runtime/core/second-nature/action/policy-bound-dispatch.d.ts +2 -0
  18. package/runtime/core/second-nature/action/policy-bound-dispatch.js +7 -3
  19. package/runtime/core/second-nature/control-plane/cycle-finalizer.d.ts +82 -0
  20. package/runtime/core/second-nature/control-plane/cycle-finalizer.js +187 -0
  21. package/runtime/core/second-nature/control-plane/heartbeat-orchestrator.js +13 -9
  22. package/runtime/core/second-nature/control-plane/real-runtime-spine.js +1 -1
  23. package/runtime/core/second-nature/guidance/guidance-proposal-consumer.d.ts +2 -1
  24. package/runtime/core/second-nature/guidance/guidance-proposal-consumer.js +4 -2
  25. package/runtime/core/second-nature/perception/judgment-engine.js +8 -4
  26. package/runtime/core/second-nature/perception/perception-builder.js +14 -2
  27. package/runtime/core/second-nature/quiet-dream/daily-rhythm-scheduler.js +30 -3
  28. package/runtime/core/second-nature/quiet-dream/dream-consolidation-runner.d.ts +5 -1
  29. package/runtime/core/second-nature/quiet-dream/dream-consolidation-runner.js +68 -29
  30. package/runtime/core/second-nature/quiet-dream/dream-scheduler.js +2 -1
  31. package/runtime/core/second-nature/quiet-dream/memory-projection-lifecycle.js +2 -1
  32. package/runtime/core/second-nature/quiet-dream/quiet-daily-review-builder.d.ts +1 -0
  33. package/runtime/core/second-nature/quiet-dream/quiet-daily-review-builder.js +33 -0
  34. package/runtime/observability/causal-loop-health.d.ts +2 -1
  35. package/runtime/observability/causal-loop-health.js +7 -0
  36. package/runtime/observability/loop-stage-event-sink.js +6 -1
  37. package/runtime/observability/loop-status.d.ts +2 -0
  38. package/runtime/observability/loop-status.js +14 -1
  39. package/runtime/observability/services/heartbeat-digest-assembler.d.ts +3 -0
  40. package/runtime/observability/services/heartbeat-digest-assembler.js +9 -0
  41. package/runtime/shared/degraded-status-classifier.d.ts +16 -0
  42. package/runtime/shared/degraded-status-classifier.js +68 -0
  43. package/runtime/shared/evidence-level-classifier.d.ts +61 -0
  44. package/runtime/shared/evidence-level-classifier.js +116 -0
  45. package/runtime/shared/provenance-tier.d.ts +37 -0
  46. package/runtime/shared/provenance-tier.js +97 -0
  47. package/runtime/shared/setup-ack.d.ts +54 -0
  48. package/runtime/shared/setup-ack.js +108 -0
  49. package/runtime/shared/source-ref-compat.js +5 -2
  50. package/runtime/shared/types/v8-contracts.d.ts +13 -2
  51. package/runtime/storage/db/index.js +71 -28
  52. package/runtime/storage/db/migrations/v8-005-single-status-schema.js +2 -2
  53. package/runtime/storage/db/migrations/v8-006-loop-stage-event-proof-trace-columns.d.ts +9 -0
  54. package/runtime/storage/db/migrations/v8-006-loop-stage-event-proof-trace-columns.js +15 -0
  55. package/runtime/storage/db/schema/v8-entities.d.ts +76 -0
  56. package/runtime/storage/db/schema/v8-entities.js +4 -0
  57. package/runtime/storage/services/write-validation-gate.js +1 -1
  58. package/runtime/storage/v8-state-stores.d.ts +7 -2
  59. package/runtime/storage/v8-state-stores.js +37 -19
@@ -44,6 +44,100 @@ import { createGoalLifecyclePolicy } from "../../core/second-nature/heartbeat/go
44
44
  import { createIdleCuriosityPolicy } from "../../core/second-nature/heartbeat/idle-curiosity-policy.js";
45
45
  import { createCircuitBreakerManager } from "../../core/second-nature/body/circuit-breaker/circuit-breaker-manager.js";
46
46
  import { createProbeSignalAdapter } from "../../core/second-nature/body/probe-signal-adapter.js";
47
+ function finalizeEnvelope(envelope, fallbackLevel = "carrier_ack") {
48
+ return {
49
+ ...envelope,
50
+ evidenceLevel: envelope.evidenceLevel ?? fallbackLevel,
51
+ };
52
+ }
53
+ function isRuntimeOpsEnvelope(value) {
54
+ return (typeof value === "object" &&
55
+ value !== null &&
56
+ "ok" in value &&
57
+ "command" in value &&
58
+ "generatedAt" in value);
59
+ }
60
+ function defaultEvidenceLevelForCommand(command, runtimeMode) {
61
+ if (runtimeMode === "host_safe_carrier" || runtimeMode === "unavailable") {
62
+ return "carrier_ack";
63
+ }
64
+ // workspace_full_runtime default: contract_smoke unless proven otherwise
65
+ if (command === "setup_ack" || command === "setup_hint") {
66
+ return command === "setup_ack" ? "state_present" : "contract_smoke";
67
+ }
68
+ return "contract_smoke";
69
+ }
70
+ function normalizeEnvelopeResult(raw, command, runtimeMode) {
71
+ if (isRuntimeOpsEnvelope(raw)) {
72
+ const mode = raw.runtimeMode ?? runtimeMode ?? "host_safe_carrier";
73
+ const fallbackSurface = raw.surfaceMode ?? (mode === "workspace_full_runtime" ? "workspace_full_runtime" : "cli");
74
+ const fallback = defaultEvidenceLevelForCommand(command, mode);
75
+ return {
76
+ ...raw,
77
+ runtimeMode: mode,
78
+ surfaceMode: fallbackSurface,
79
+ evidenceLevel: raw.evidenceLevel ?? fallback,
80
+ };
81
+ }
82
+ // Partial result (e.g. goal, connector_status, connector_test) — wrap into a valid envelope
83
+ // while preserving the caller's data and error shape.
84
+ if (typeof raw === "object" &&
85
+ raw !== null &&
86
+ "ok" in raw &&
87
+ typeof raw.ok === "boolean" &&
88
+ "command" in raw &&
89
+ typeof raw.command === "string") {
90
+ const partial = raw;
91
+ const mode = runtimeMode ?? partial.runtimeMode ?? "host_safe_carrier";
92
+ const fallbackSurface = partial.surfaceMode ??
93
+ (mode === "workspace_full_runtime" ? "workspace_full_runtime" : "cli");
94
+ const fallback = defaultEvidenceLevelForCommand(command, mode);
95
+ return {
96
+ ...partial,
97
+ runtimeMode: mode,
98
+ surfaceMode: fallbackSurface,
99
+ generatedAt: new Date().toISOString(),
100
+ warnings: Array.isArray(partial.warnings) ? partial.warnings : [],
101
+ sourceRefs: Array.isArray(partial.sourceRefs) ? partial.sourceRefs : [],
102
+ evidenceLevel: partial.evidenceLevel ?? fallback,
103
+ };
104
+ }
105
+ // Non-envelope result (e.g. plain error from an internal helper) — wrap honestly,
106
+ // but preserve any structured error already present on the raw object.
107
+ const generatedAt = new Date().toISOString();
108
+ const existingError = (() => {
109
+ if (typeof raw === "object" &&
110
+ raw !== null &&
111
+ "error" in raw &&
112
+ raw.error !== null &&
113
+ typeof raw.error === "object" &&
114
+ "code" in raw.error) {
115
+ const err = raw.error;
116
+ return {
117
+ code: err.code,
118
+ message: err.message ?? "Internal ops error",
119
+ nextStep: err.nextStep,
120
+ };
121
+ }
122
+ return undefined;
123
+ })();
124
+ return {
125
+ ok: false,
126
+ command,
127
+ runtimeMode: runtimeMode ?? "host_safe_carrier",
128
+ surfaceMode: runtimeMode === "workspace_full_runtime" ? "workspace_full_runtime" : "cli",
129
+ generatedAt,
130
+ error: existingError ?? {
131
+ code: "OPS_RESULT_NOT_AN_ENVELOPE",
132
+ message: typeof raw === "object" && raw !== null && "message" in raw
133
+ ? String(raw.message)
134
+ : "Internal ops result did not match RuntimeOpsEnvelope shape",
135
+ },
136
+ warnings: [],
137
+ sourceRefs: [],
138
+ evidenceLevel: "carrier_ack",
139
+ };
140
+ }
47
141
  function coerceProbeOnlyFlag(input) {
48
142
  const v = input?.probeOnly;
49
143
  return v === true || v === "true" || v === 1 || v === "1";
@@ -360,1294 +454,1328 @@ export function createOpsRouter(deps) {
360
454
  dreamSchedulePort: input.dreamSchedulePort,
361
455
  }),
362
456
  async dispatch(command, input) {
363
- if (command === "heartbeat_check") {
364
- const runtimeAvailable = typeof input?.runtimeAvailable === "boolean"
365
- ? input.runtimeAvailable
366
- : deps.runtimeAvailable;
367
- // v7 T-V7C.C.2: assemble affordance map and experience writer for breaker-aware heartbeat.
368
- let affordanceMap;
369
- if (deps.toolAffordancePort) {
370
- try {
371
- affordanceMap = await deps.toolAffordancePort.assembleAffordanceMap({});
372
- }
373
- catch {
374
- // degrade gracefully; guard-layer will skip breaker check without affordanceMap
375
- }
376
- }
377
- let experienceWriter;
378
- if (deps.state) {
379
- experienceWriter = createExperienceWriter(createToolExperienceStore(deps.state));
380
- }
381
- // v7 T-V7C.C.6: assemble digest opts when auditStore is wired.
382
- let digestOpts;
383
- if (deps.auditStore) {
384
- digestOpts = {
385
- assemblerDeps: {
386
- auditStore: deps.auditStore,
387
- ...deps.heartbeatDigestDeps,
388
- },
389
- };
390
- }
391
- // v7 T-V7C.C.6: assemble dream schedule port when state DB is wired.
392
- let dreamSchedulePort;
393
- if (deps.state) {
394
- dreamSchedulePort = createQuietDreamSchedulePort(deps.state);
395
- }
396
- // v7 T-CP.C.3: assemble goal lifecycle and idle curiosity policies.
397
- const goalLifecyclePolicy = createGoalLifecyclePolicy();
398
- const idleCuriosityPolicy = createIdleCuriosityPolicy();
399
- // v7 T-BTS.C.5: assemble circuit breaker manager when state DB is wired.
400
- let circuitBreakerManager;
401
- if (deps.state) {
402
- const probeResultStore = createCapabilityProbeResultStore(deps.state);
403
- const toolExpStore = createToolExperienceStore(deps.state);
404
- const probeAdapter = createProbeSignalAdapter({
405
- wetProbeRunner: createWetProbeRunner(),
406
- probeResultStore,
407
- toolExperienceStore: toolExpStore,
408
- });
409
- const registryV7 = new CapabilityContractRegistryV7();
410
- circuitBreakerManager = createCircuitBreakerManager({
411
- database: deps.state,
412
- probeAdapter,
413
- registry: registryV7,
414
- });
415
- }
416
- try {
417
- const result = await heartbeatCheck({
418
- probeOnly: coerceProbeOnlyFlag(input),
419
- runtimeAvailable,
420
- fakeControlPlanePassthrough: input?.fakeControlPlanePassthrough &&
421
- typeof input.fakeControlPlanePassthrough === "object"
422
- ? input.fakeControlPlanePassthrough
423
- : undefined,
424
- readModels: input?.readModels ??
425
- deps.readModels,
426
- runtimeRecorder: input
427
- ?.runtimeRecorder ?? deps.runtimeRecorder,
428
- state: input?.state ??
429
- deps.state,
430
- workspaceRoot: input
431
- ?.workspaceRoot ?? deps.workspaceRoot,
432
- timestamp: typeof input?.timestamp === "string" ? input.timestamp : undefined,
433
- sessionContext: typeof input?.sessionContext === "string"
434
- ? input.sessionContext
435
- : undefined,
436
- scopeHint: input?.scopeHint,
437
- connectorExecutor: input
438
- ?.connectorExecutor ?? deps.connectorExecutor,
439
- connectorRegistry: input
440
- ?.connectorRegistry ?? deps.connectorRegistry,
441
- affordanceMap,
442
- experienceWriter,
443
- digestOpts,
444
- dreamSchedulePort,
445
- auditStore: deps.auditStore,
446
- goalLifecyclePolicy,
447
- idleCuriosityPolicy,
448
- circuitBreakerManager,
449
- v8SpineEnabled: input
450
- ?.v8SpineEnabled ?? (deps.state !== undefined),
451
- });
452
- if (result.ok &&
453
- result.surfaceMode === "workspace_full_runtime" &&
454
- !coerceProbeOnlyFlag(input) &&
455
- deps.state &&
456
- deps.restoreSnapshotStore) {
457
+ const rawResult = await (async () => {
458
+ if (command === "heartbeat") {
459
+ // T-CP.R.5: legacy v7 heartbeat command is no longer the operator-facing model.
460
+ // It still runs through heartbeat_check for backward compatibility, but the
461
+ // canonical operator-facing command is `heartbeat_check` (v8 living-loop spine).
462
+ return (async () => {
457
463
  try {
458
- const capture = await captureRuntimeSnapshot(deps, {
459
- snapshotId: `heartbeat:${result.decisionId ?? "cycle"}:${Date.now()}`,
460
- subjectId: result.decisionId ?? "heartbeat_check",
461
- reasonCode: "heartbeat_check",
462
- summaryText: `Heartbeat ${result.status} captured bounded restore snapshot`,
463
- focus: result.status,
464
- progress: result.reasons.join(",") || "heartbeat_completed",
465
- nextIntent: "continue_runtime_loop",
466
- sourceRefs: result.decisionId
467
- ? [`heartbeat:${result.decisionId}`]
468
- : ["heartbeat:runtime"],
469
- });
470
- if (capture.ok) {
471
- result.reasons = [...result.reasons, "restore_snapshot_captured"];
472
- }
464
+ const result = await this.dispatch("heartbeat_check", input);
465
+ return {
466
+ ...result,
467
+ warnings: [
468
+ ...(Array.isArray(result.warnings) ? result.warnings : []),
469
+ {
470
+ code: "LEGACY_HEARTBEAT_DEPRECATED",
471
+ message: "`heartbeat` is deprecated; use `heartbeat_check` (v8 living-loop spine)",
472
+ },
473
+ ],
474
+ };
473
475
  }
474
476
  catch (err) {
475
477
  const msg = err instanceof Error ? err.message : String(err);
476
- result.reasons = [...result.reasons, `restore_snapshot_capture_failed:${msg}`];
477
- }
478
- }
479
- return result;
480
- }
481
- catch (err) {
482
- const msg = err instanceof Error ? err.message : String(err);
483
- const envelope = {
484
- ok: false,
485
- command: "heartbeat_check",
486
- runtimeMode: runtimeAvailable ? "workspace_full_runtime" : "unavailable",
487
- surfaceMode: "cli",
488
- generatedAt: new Date().toISOString(),
489
- error: {
490
- code: "HEARTBEAT_CYCLE_EXCEPTION",
491
- message: `heartbeat_check cycle threw unexpectedly: ${msg.slice(0, 200)}`,
492
- nextStep: "check_logs_and_report",
493
- },
494
- warnings: [],
495
- sourceRefs: [],
496
- };
497
- return envelope;
498
- }
499
- }
500
- if (command === "fallback") {
501
- const ref = typeof input?.ref === "string" ? input.ref.trim() : "";
502
- if (!ref) {
503
- return {
504
- ok: false,
505
- error: {
506
- code: "MISSING_FALLBACK_REF",
507
- message: "fallback requires args.ref (e.g. fallback:…)",
508
- requiredUserInput: ["ref"],
509
- nextStep: "reinvoke_with_ref",
510
- },
511
- };
512
- }
513
- if (!deps.readModels?.loadFallbackView) {
514
- return {
515
- ok: false,
516
- error: {
517
- code: "FALLBACK_READ_MODEL_UNAVAILABLE",
518
- message: "Operator fallback view requires workspace read models",
519
- requiredUserInput: ["ref"],
520
- nextStep: "wire_read_models_into_ops_router",
521
- },
522
- };
523
- }
524
- return (async () => {
525
- try {
526
- const data = await showOperatorFallback(ref, deps.readModels);
527
- return { ok: true, command: "fallback", data };
528
- }
529
- catch (error) {
530
- if (error instanceof OperatorFallbackNotFoundError) {
531
478
  return {
532
479
  ok: false,
533
- command: "fallback",
480
+ command: "heartbeat",
534
481
  error: {
535
- code: error.code,
536
- message: error.message,
537
- requiredUserInput: ["ref"],
538
- nextStep: "verify_fallback_ref_from_delivery_audit",
482
+ code: "HEARTBEAT_CYCLE_EXCEPTION",
483
+ message: msg.slice(0, 200),
484
+ nextStep: "use_heartbeat_check_command",
539
485
  },
540
486
  };
541
487
  }
542
- throw error;
488
+ })();
489
+ }
490
+ if (command === "heartbeat_check") {
491
+ const runtimeAvailable = typeof input?.runtimeAvailable === "boolean"
492
+ ? input.runtimeAvailable
493
+ : deps.runtimeAvailable;
494
+ // v7 T-V7C.C.2: assemble affordance map and experience writer for breaker-aware heartbeat.
495
+ let affordanceMap;
496
+ if (deps.toolAffordancePort) {
497
+ try {
498
+ affordanceMap = await deps.toolAffordancePort.assembleAffordanceMap({});
499
+ }
500
+ catch {
501
+ // degrade gracefully; guard-layer will skip breaker check without affordanceMap
502
+ }
543
503
  }
544
- })();
545
- }
546
- if (command === "capability_probe") {
547
- // T1.2.8 (SN-CODE-03): run host capability probe with static unknown adapter (CLI context).
548
- // Persists report when observabilityDb is available; returns safe JSON subset.
549
- return (async () => {
550
- const adapter = createStaticUnknownAdapter(deps.workspaceRoot);
551
- const docCheckedAt = new Date().toISOString();
552
- const report = probeHostCapability({
553
- adapter,
554
- docLinks: [],
555
- docCheckedAt,
556
- });
557
- if (deps.observabilityDb) {
558
- await recordHostCapability(deps.observabilityDb, report);
504
+ let experienceWriter;
505
+ if (deps.state) {
506
+ experienceWriter = createExperienceWriter(createToolExperienceStore(deps.state));
559
507
  }
560
- return {
561
- ok: true,
562
- command: "capability_probe",
563
- data: {
564
- reportId: report.reportId,
565
- generatedAt: report.generatedAt,
566
- deliveryTarget: report.deliveryTarget,
567
- pluginLoad: { verdict: report.pluginLoad.verdict },
568
- heartbeatBridge: { verdict: report.heartbeatBridge.verdict },
569
- heartbeatToolInvocation: {
570
- verdict: report.heartbeatToolInvocation.verdict,
508
+ // v7 T-V7C.C.6: assemble digest opts when auditStore is wired.
509
+ let digestOpts;
510
+ if (deps.auditStore) {
511
+ digestOpts = {
512
+ assemblerDeps: {
513
+ auditStore: deps.auditStore,
514
+ ...deps.heartbeatDigestDeps,
571
515
  },
572
- ackDropBehavior: { verdict: report.ackDropBehavior.verdict },
573
- conflictCount: report.conflictRecords.length,
574
- recommendedNextStep: report.recommendedNextStep,
575
- note: "static_local_probe: all verdicts are unknown without live host context",
576
- },
577
- };
578
- })();
579
- }
580
- if (command === "near_real_smoke") {
581
- // T3.3.2 (SN-CODE-05): wrap runNearRealConnectorSmoke as an ops surface command.
582
- // Requires state + observabilityDb + workspaceRoot to be wired into OpsRouterDeps.
583
- if (!deps.state || !deps.observabilityDb || !deps.workspaceRoot) {
584
- return {
585
- ok: false,
586
- command: "near_real_smoke",
587
- error: {
588
- code: "NEAR_REAL_SMOKE_DEPS_UNAVAILABLE",
589
- message: "near_real_smoke requires state, observabilityDb, and workspaceRoot in OpsRouterDeps",
590
- nextStep: "wire_deps_into_ops_router",
591
- },
592
- };
516
+ };
517
+ }
518
+ // v7 T-V7C.C.6: assemble dream schedule port when state DB is wired.
519
+ let dreamSchedulePort;
520
+ if (deps.state) {
521
+ dreamSchedulePort = createQuietDreamSchedulePort(deps.state);
522
+ }
523
+ // v7 T-CP.C.3: assemble goal lifecycle and idle curiosity policies.
524
+ const goalLifecyclePolicy = createGoalLifecyclePolicy();
525
+ const idleCuriosityPolicy = createIdleCuriosityPolicy();
526
+ // v7 T-BTS.C.5: assemble circuit breaker manager when state DB is wired.
527
+ let circuitBreakerManager;
528
+ if (deps.state) {
529
+ const probeResultStore = createCapabilityProbeResultStore(deps.state);
530
+ const toolExpStore = createToolExperienceStore(deps.state);
531
+ const probeAdapter = createProbeSignalAdapter({
532
+ wetProbeRunner: createWetProbeRunner(),
533
+ probeResultStore,
534
+ toolExperienceStore: toolExpStore,
535
+ });
536
+ const registryV7 = new CapabilityContractRegistryV7();
537
+ circuitBreakerManager = createCircuitBreakerManager({
538
+ database: deps.state,
539
+ probeAdapter,
540
+ registry: registryV7,
541
+ });
542
+ }
543
+ try {
544
+ const result = await heartbeatCheck({
545
+ probeOnly: coerceProbeOnlyFlag(input),
546
+ runtimeAvailable,
547
+ fakeControlPlanePassthrough: input?.fakeControlPlanePassthrough &&
548
+ typeof input.fakeControlPlanePassthrough === "object"
549
+ ? input.fakeControlPlanePassthrough
550
+ : undefined,
551
+ readModels: input?.readModels ??
552
+ deps.readModels,
553
+ runtimeRecorder: input
554
+ ?.runtimeRecorder ?? deps.runtimeRecorder,
555
+ state: input?.state ??
556
+ deps.state,
557
+ workspaceRoot: input
558
+ ?.workspaceRoot ?? deps.workspaceRoot,
559
+ timestamp: typeof input?.timestamp === "string" ? input.timestamp : undefined,
560
+ sessionContext: typeof input?.sessionContext === "string"
561
+ ? input.sessionContext
562
+ : undefined,
563
+ scopeHint: input?.scopeHint,
564
+ connectorExecutor: input
565
+ ?.connectorExecutor ?? deps.connectorExecutor,
566
+ connectorRegistry: input
567
+ ?.connectorRegistry ?? deps.connectorRegistry,
568
+ affordanceMap,
569
+ experienceWriter,
570
+ digestOpts,
571
+ dreamSchedulePort,
572
+ auditStore: deps.auditStore,
573
+ goalLifecyclePolicy,
574
+ idleCuriosityPolicy,
575
+ circuitBreakerManager,
576
+ v8SpineEnabled: input
577
+ ?.v8SpineEnabled ?? (deps.state !== undefined),
578
+ });
579
+ if (result.ok &&
580
+ result.surfaceMode === "workspace_full_runtime" &&
581
+ !coerceProbeOnlyFlag(input) &&
582
+ deps.state &&
583
+ deps.restoreSnapshotStore) {
584
+ try {
585
+ const capture = await captureRuntimeSnapshot(deps, {
586
+ snapshotId: `heartbeat:${result.decisionId ?? "cycle"}:${Date.now()}`,
587
+ subjectId: result.decisionId ?? "heartbeat_check",
588
+ reasonCode: "heartbeat_check",
589
+ summaryText: `Heartbeat ${result.status} captured bounded restore snapshot`,
590
+ focus: result.status,
591
+ progress: result.reasons.join(",") || "heartbeat_completed",
592
+ nextIntent: "continue_runtime_loop",
593
+ sourceRefs: result.decisionId
594
+ ? [`heartbeat:${result.decisionId}`]
595
+ : ["heartbeat:runtime"],
596
+ });
597
+ if (capture.ok) {
598
+ result.reasons = [...result.reasons, "restore_snapshot_captured"];
599
+ }
600
+ }
601
+ catch (err) {
602
+ const msg = err instanceof Error ? err.message : String(err);
603
+ result.reasons = [...result.reasons, `restore_snapshot_capture_failed:${msg}`];
604
+ }
605
+ }
606
+ const heartbeatEvidenceLevel = (() => {
607
+ if (!runtimeAvailable || result.surfaceMode === "host_safe_carrier" || result.status === "runtime_carrier_only") {
608
+ return "carrier_ack";
609
+ }
610
+ if (result.schemaParityOnly || coerceProbeOnlyFlag(input)) {
611
+ return "contract_smoke";
612
+ }
613
+ if (result.v8Spine?.cycleId && (result.v8Spine.closureRef || result.v8Spine.noActionReason)) {
614
+ return "real_runtime";
615
+ }
616
+ return "contract_smoke";
617
+ })();
618
+ const heartbeatEnvelope = {
619
+ ...result,
620
+ command: "heartbeat_check",
621
+ runtimeMode: runtimeAvailable ? "workspace_full_runtime" : "host_safe_carrier",
622
+ surfaceMode: result.surfaceMode,
623
+ generatedAt: new Date().toISOString(),
624
+ data: result,
625
+ warnings: [],
626
+ sourceRefs: result.decisionId ? [`heartbeat:${result.decisionId}`] : ["heartbeat:runtime"],
627
+ evidenceLevel: heartbeatEvidenceLevel,
628
+ };
629
+ return heartbeatEnvelope;
630
+ }
631
+ catch (err) {
632
+ const msg = err instanceof Error ? err.message : String(err);
633
+ const envelope = {
634
+ ok: false,
635
+ command: "heartbeat_check",
636
+ runtimeMode: runtimeAvailable ? "workspace_full_runtime" : "unavailable",
637
+ surfaceMode: runtimeAvailable ? "workspace_full_runtime" : "cli",
638
+ generatedAt: new Date().toISOString(),
639
+ error: {
640
+ code: "HEARTBEAT_CYCLE_EXCEPTION",
641
+ message: `heartbeat_check cycle threw unexpectedly: ${msg.slice(0, 200)}`,
642
+ nextStep: "check_logs_and_report",
643
+ },
644
+ warnings: [],
645
+ sourceRefs: [],
646
+ };
647
+ return envelope;
648
+ }
593
649
  }
594
- return (async () => {
595
- const result = await runNearRealConnectorSmoke({
596
- state: deps.state,
597
- observabilityDb: deps.observabilityDb,
598
- workspaceRoot: deps.workspaceRoot,
599
- });
600
- return {
601
- ok: true,
602
- command: "near_real_smoke",
603
- data: result,
604
- };
605
- })();
606
- }
607
- if (command === "connector_init") {
608
- // T1.3.1 (SN-CODE-06): generate connector manifest stub.
609
- return (async () => {
610
- const result = await connectorInit({
611
- platformId: typeof input?.platformId === "string" ? input.platformId : "",
612
- family: typeof input?.family === "string"
613
- ? input.family
614
- : undefined,
615
- displayName: typeof input?.displayName === "string" ? input.displayName : undefined,
616
- runnerKind: typeof input?.runnerKind === "string"
617
- ? input.runnerKind
618
- : undefined,
619
- force: Boolean(input?.force),
620
- workspaceRoot: deps.workspaceRoot,
621
- });
622
- return result;
623
- })();
624
- }
625
- if (command === "connector_behavior_add") {
626
- return connectorBehaviorAdd({
627
- platformId: typeof input?.platformId === "string" ? input.platformId : "",
628
- behaviorId: typeof input?.behaviorId === "string"
629
- ? input.behaviorId
630
- : typeof input?.capabilityId === "string"
631
- ? input.capabilityId
632
- : "",
633
- description: typeof input?.description === "string" ? input.description : undefined,
634
- channel: typeof input?.channel === "string" ? input.channel : undefined,
635
- sourceRefs: input?.sourceRefs,
636
- observedCount: typeof input?.observedCount === "number" ? input.observedCount : undefined,
637
- workspaceRoot: typeof input?.workspaceRoot === "string"
638
- ? input.workspaceRoot
639
- : deps.workspaceRoot,
640
- });
641
- }
642
- if (command === "connector_status") {
643
- return connectorStatus(deps.registry, undefined, {
644
- includeHealth: Boolean(input?.includeHealth),
645
- workspaceRoot: typeof input?.workspaceRoot === "string"
646
- ? input.workspaceRoot
647
- : deps.workspaceRoot,
648
- });
649
- }
650
- if (command === "connector_test") {
651
- // v7 T-V7C.C.1: dryRun=false is the canonical wet probe switch.
652
- const isWet = input?.wet === true ||
653
- input?.wet === "true" ||
654
- input?.dryRun === false ||
655
- input?.dryRun === "false";
656
- const result = await connectorTest(deps.registry, {
657
- platformId: typeof input?.platformId === "string" ? input.platformId : "",
658
- dryRun: isWet ? false : (input?.dryRun === false ? false : true),
659
- workspaceRoot: typeof input?.workspaceRoot === "string"
660
- ? input.workspaceRoot
661
- : deps.workspaceRoot,
662
- });
663
- if (!isWet || !result.ok) {
664
- return result;
650
+ if (command === "fallback") {
651
+ const ref = typeof input?.ref === "string" ? input.ref.trim() : "";
652
+ if (!ref) {
653
+ return {
654
+ ok: false,
655
+ command: "fallback",
656
+ error: {
657
+ code: "MISSING_FALLBACK_REF",
658
+ message: "fallback requires args.ref (e.g. fallback:…)",
659
+ requiredUserInput: ["ref"],
660
+ nextStep: "reinvoke_with_ref",
661
+ },
662
+ };
663
+ }
664
+ if (!deps.readModels?.loadFallbackView) {
665
+ return {
666
+ ok: false,
667
+ error: {
668
+ code: "FALLBACK_READ_MODEL_UNAVAILABLE",
669
+ message: "Operator fallback view requires workspace read models",
670
+ requiredUserInput: ["ref"],
671
+ nextStep: "wire_read_models_into_ops_router",
672
+ },
673
+ };
674
+ }
675
+ return (async () => {
676
+ try {
677
+ const data = await showOperatorFallback(ref, deps.readModels);
678
+ return { ok: true, command: "fallback", data };
679
+ }
680
+ catch (error) {
681
+ if (error instanceof OperatorFallbackNotFoundError) {
682
+ return {
683
+ ok: false,
684
+ command: "fallback",
685
+ error: {
686
+ code: error.code,
687
+ message: error.message,
688
+ requiredUserInput: ["ref"],
689
+ nextStep: "verify_fallback_ref_from_delivery_audit",
690
+ },
691
+ };
692
+ }
693
+ throw error;
694
+ }
695
+ })();
665
696
  }
666
- const data = result.data && typeof result.data === "object"
667
- ? result.data
668
- : {};
669
- const capabilities = Array.isArray(data.capabilities)
670
- ? data.capabilities.filter((item) => typeof item === "string")
671
- : [];
672
- const capabilityId = textInput(input, "capabilityId") ?? capabilities[0] ?? "";
673
- if (!capabilityId) {
674
- return {
675
- ok: false,
676
- command: "connector_test",
677
- error: {
678
- code: "MISSING_CAPABILITY_ID",
679
- message: "wet connector_test requires capabilityId or at least one connector capability",
680
- requiredUserInput: ["capabilityId"],
681
- nextStep: "reinvoke_with_capability_id",
682
- },
683
- };
697
+ if (command === "capability_probe") {
698
+ // T1.2.8 (SN-CODE-03): run host capability probe with static unknown adapter (CLI context).
699
+ // Persists report when observabilityDb is available; returns safe JSON subset.
700
+ return (async () => {
701
+ const adapter = createStaticUnknownAdapter(deps.workspaceRoot);
702
+ const docCheckedAt = new Date().toISOString();
703
+ const report = probeHostCapability({
704
+ adapter,
705
+ docLinks: [],
706
+ docCheckedAt,
707
+ });
708
+ if (deps.observabilityDb) {
709
+ await recordHostCapability(deps.observabilityDb, report);
710
+ }
711
+ return {
712
+ ok: true,
713
+ command: "capability_probe",
714
+ data: {
715
+ reportId: report.reportId,
716
+ generatedAt: report.generatedAt,
717
+ deliveryTarget: report.deliveryTarget,
718
+ pluginLoad: { verdict: report.pluginLoad.verdict },
719
+ heartbeatBridge: { verdict: report.heartbeatBridge.verdict },
720
+ heartbeatToolInvocation: {
721
+ verdict: report.heartbeatToolInvocation.verdict,
722
+ },
723
+ ackDropBehavior: { verdict: report.ackDropBehavior.verdict },
724
+ conflictCount: report.conflictRecords.length,
725
+ recommendedNextStep: report.recommendedNextStep,
726
+ note: "static_local_probe: all verdicts are unknown without live host context",
727
+ },
728
+ };
729
+ })();
730
+ }
731
+ if (command === "near_real_smoke") {
732
+ // T3.3.2 (SN-CODE-05): wrap runNearRealConnectorSmoke as an ops surface command.
733
+ // Requires state + observabilityDb + workspaceRoot to be wired into OpsRouterDeps.
734
+ if (!deps.state || !deps.observabilityDb || !deps.workspaceRoot) {
735
+ return {
736
+ ok: false,
737
+ command: "near_real_smoke",
738
+ error: {
739
+ code: "NEAR_REAL_SMOKE_DEPS_UNAVAILABLE",
740
+ message: "near_real_smoke requires state, observabilityDb, and workspaceRoot in OpsRouterDeps",
741
+ nextStep: "wire_deps_into_ops_router",
742
+ },
743
+ };
744
+ }
745
+ return (async () => {
746
+ const result = await runNearRealConnectorSmoke({
747
+ state: deps.state,
748
+ observabilityDb: deps.observabilityDb,
749
+ workspaceRoot: deps.workspaceRoot,
750
+ });
751
+ return {
752
+ ok: true,
753
+ command: "near_real_smoke",
754
+ data: result,
755
+ };
756
+ })();
684
757
  }
685
- const platformId = String(data.platformId ?? input?.platformId ?? "");
686
- const registryEntry = deps.registry?.describeConnector(platformId);
687
- if (!registryEntry) {
688
- return result;
758
+ if (command === "connector_init") {
759
+ // T1.3.1 (SN-CODE-06): generate connector manifest stub.
760
+ return (async () => {
761
+ const result = await connectorInit({
762
+ platformId: typeof input?.platformId === "string" ? input.platformId : "",
763
+ family: typeof input?.family === "string"
764
+ ? input.family
765
+ : undefined,
766
+ displayName: typeof input?.displayName === "string" ? input.displayName : undefined,
767
+ runnerKind: typeof input?.runnerKind === "string"
768
+ ? input.runnerKind
769
+ : undefined,
770
+ force: Boolean(input?.force),
771
+ workspaceRoot: deps.workspaceRoot,
772
+ });
773
+ return { command: "connector_init", ...result };
774
+ })();
689
775
  }
690
- const registryV7 = new CapabilityContractRegistryV7();
691
- registerConnectorForWetProbe({
692
- registryV7,
693
- entry: {
694
- platformId: registryEntry.platformId,
695
- capabilities: registryEntry.capabilities,
696
- manifestPath: registryEntry.manifestPath,
697
- },
698
- workspaceRoot: typeof input?.workspaceRoot === "string"
699
- ? input.workspaceRoot
700
- : deps.workspaceRoot,
701
- selectedCapabilityId: capabilityId,
702
- safeEndpoint: textInput(input, "safeEndpoint"),
703
- });
704
- const wetResult = await createWetProbeRunner().runWetProbe(platformId, capabilityId, registryV7);
705
- const warnings = [];
706
- let persistedProbeResult = false;
707
- if (deps.state) {
708
- await createCapabilityProbeResultStore(deps.state).appendProbeResult(wetResult.probeResult);
709
- persistedProbeResult = true;
776
+ if (command === "connector_behavior_add") {
777
+ return connectorBehaviorAdd({
778
+ platformId: typeof input?.platformId === "string" ? input.platformId : "",
779
+ behaviorId: typeof input?.behaviorId === "string"
780
+ ? input.behaviorId
781
+ : typeof input?.capabilityId === "string"
782
+ ? input.capabilityId
783
+ : "",
784
+ description: typeof input?.description === "string" ? input.description : undefined,
785
+ channel: typeof input?.channel === "string" ? input.channel : undefined,
786
+ sourceRefs: input?.sourceRefs,
787
+ observedCount: typeof input?.observedCount === "number" ? input.observedCount : undefined,
788
+ workspaceRoot: typeof input?.workspaceRoot === "string"
789
+ ? input.workspaceRoot
790
+ : deps.workspaceRoot,
791
+ });
710
792
  }
711
- else {
712
- warnings.push("state_db_unavailable:capability_probe_result_not_persisted");
793
+ if (command === "connector_status") {
794
+ return connectorStatus(deps.registry, undefined, {
795
+ includeHealth: Boolean(input?.includeHealth),
796
+ workspaceRoot: typeof input?.workspaceRoot === "string"
797
+ ? input.workspaceRoot
798
+ : deps.workspaceRoot,
799
+ });
713
800
  }
714
- return {
715
- // T-V7C.C.5: only "available" (HTTP 200-299) counts as success;
716
- // "degraded" (429/503) and "unavailable" both result in ok=false.
717
- ok: wetResult.probeResult.actualStatus === "available",
718
- command: "connector_test",
719
- data: {
720
- ...data,
721
- dryRun: false,
722
- capabilityId,
723
- actualStatus: wetResult.probeResult.actualStatus,
724
- httpStatus: wetResult.probeResult.httpStatus ?? wetResult.httpStatus,
725
- probeResultId: wetResult.probeResult.probeResultId,
726
- probeConfigRef: wetResult.probeResult.probeConfigRef,
727
- sampleResponseRef: wetResult.probeResult.sampleResponseRef,
728
- persistedProbeResult,
729
- triggerSource: "manual_run",
730
- affectsHeartbeatCadence: false,
731
- note: "wet probe mode: executed safe probe endpoint and persisted capability_probe_result when state DB is available",
732
- },
733
- warnings,
734
- };
735
- }
736
- if (command === "connector:run") {
737
- // T-ROS.C.3: manual connector execution — isolated from heartbeat cadence
738
- const platformId = typeof input?.platformId === "string" ? input.platformId : "";
739
- const capabilityId = typeof input?.capabilityId === "string" ? input.capabilityId : "";
740
- if (!platformId || !capabilityId) {
741
- return {
742
- ok: false,
743
- command: "connector:run",
744
- error: {
745
- code: "MISSING_PLATFORM_OR_CAPABILITY_ID",
746
- message: "connector:run requires platformId and capabilityId",
747
- requiredUserInput: ["platformId", "capabilityId"],
748
- nextStep: "reinvoke_with_platform_and_capability_id",
801
+ if (command === "connector_test") {
802
+ // v7 T-V7C.C.1: dryRun=false is the canonical wet probe switch.
803
+ const isWet = input?.wet === true ||
804
+ input?.wet === "true" ||
805
+ input?.dryRun === false ||
806
+ input?.dryRun === "false";
807
+ const result = await connectorTest(deps.registry, {
808
+ platformId: typeof input?.platformId === "string" ? input.platformId : "",
809
+ dryRun: isWet ? false : (input?.dryRun === false ? false : true),
810
+ workspaceRoot: typeof input?.workspaceRoot === "string"
811
+ ? input.workspaceRoot
812
+ : deps.workspaceRoot,
813
+ });
814
+ if (!isWet || !result.ok) {
815
+ return result;
816
+ }
817
+ const data = result.data && typeof result.data === "object"
818
+ ? result.data
819
+ : {};
820
+ const capabilities = Array.isArray(data.capabilities)
821
+ ? data.capabilities.filter((item) => typeof item === "string")
822
+ : [];
823
+ const capabilityId = textInput(input, "capabilityId") ?? capabilities[0] ?? "";
824
+ if (!capabilityId) {
825
+ return {
826
+ ok: false,
827
+ command: "connector_test",
828
+ error: {
829
+ code: "MISSING_CAPABILITY_ID",
830
+ message: "wet connector_test requires capabilityId or at least one connector capability",
831
+ requiredUserInput: ["capabilityId"],
832
+ nextStep: "reinvoke_with_capability_id",
833
+ },
834
+ };
835
+ }
836
+ const platformId = String(data.platformId ?? input?.platformId ?? "");
837
+ const registryEntry = deps.registry?.describeConnector(platformId);
838
+ if (!registryEntry) {
839
+ return result;
840
+ }
841
+ const registryV7 = new CapabilityContractRegistryV7();
842
+ registerConnectorForWetProbe({
843
+ registryV7,
844
+ entry: {
845
+ platformId: registryEntry.platformId,
846
+ capabilities: registryEntry.capabilities,
847
+ manifestPath: registryEntry.manifestPath,
749
848
  },
750
- };
751
- }
752
- if (!deps.connectorExecutor || !deps.state) {
849
+ workspaceRoot: typeof input?.workspaceRoot === "string"
850
+ ? input.workspaceRoot
851
+ : deps.workspaceRoot,
852
+ selectedCapabilityId: capabilityId,
853
+ safeEndpoint: textInput(input, "safeEndpoint"),
854
+ });
855
+ const wetResult = await createWetProbeRunner().runWetProbe(platformId, capabilityId, registryV7);
856
+ const warnings = [];
857
+ let persistedProbeResult = false;
858
+ if (deps.state) {
859
+ await createCapabilityProbeResultStore(deps.state).appendProbeResult(wetResult.probeResult);
860
+ persistedProbeResult = true;
861
+ }
862
+ else {
863
+ warnings.push("state_db_unavailable:capability_probe_result_not_persisted");
864
+ }
753
865
  return {
754
- ok: false,
755
- command: "connector:run",
756
- error: {
757
- code: "MANUAL_RUN_DEPS_UNAVAILABLE",
758
- message: "connector:run requires connectorExecutor and state database",
759
- nextStep: "wire_connector_executor_and_state_into_ops_router",
866
+ // T-V7C.C.5: only "available" (HTTP 200-299) counts as success;
867
+ // "degraded" (429/503) and "unavailable" both result in ok=false.
868
+ ok: wetResult.probeResult.actualStatus === "available",
869
+ command: "connector_test",
870
+ data: {
871
+ ...data,
872
+ dryRun: false,
873
+ capabilityId,
874
+ actualStatus: wetResult.probeResult.actualStatus,
875
+ httpStatus: wetResult.probeResult.httpStatus ?? wetResult.httpStatus,
876
+ probeResultId: wetResult.probeResult.probeResultId,
877
+ probeConfigRef: wetResult.probeResult.probeConfigRef,
878
+ sampleResponseRef: wetResult.probeResult.sampleResponseRef,
879
+ persistedProbeResult,
880
+ triggerSource: "manual_run",
881
+ affectsHeartbeatCadence: false,
882
+ note: "wet probe mode: executed safe probe endpoint and persisted capability_probe_result when state DB is available",
760
883
  },
884
+ warnings,
761
885
  };
762
886
  }
763
- const toolExperienceStore = createToolExperienceStore(deps.state);
764
- const experienceWriter = createExperienceWriter(toolExperienceStore);
765
- const wetProbeRunner = createWetProbeRunner();
766
- const registryV7 = new CapabilityContractRegistryV7();
767
- // Populate V7 registry from dynamic registry if available (best-effort)
768
- if (deps.registry) {
769
- for (const entry of deps.registry.listConnectors()) {
770
- if (entry.manifestPath) {
771
- try {
772
- const manifestText = fs.readFileSync(entry.manifestPath, "utf-8");
773
- const manifest = JSON.parse(manifestText);
774
- registryV7.register(manifest);
775
- }
776
- catch {
777
- // Skip manifests that can't be read or don't validate as V7
887
+ if (command === "connector:run") {
888
+ // T-ROS.C.3: manual connector execution — isolated from heartbeat cadence
889
+ const platformId = typeof input?.platformId === "string" ? input.platformId : "";
890
+ const capabilityId = typeof input?.capabilityId === "string" ? input.capabilityId : "";
891
+ if (!platformId || !capabilityId) {
892
+ return {
893
+ ok: false,
894
+ command: "connector:run",
895
+ error: {
896
+ code: "MISSING_PLATFORM_OR_CAPABILITY_ID",
897
+ message: "connector:run requires platformId and capabilityId",
898
+ requiredUserInput: ["platformId", "capabilityId"],
899
+ nextStep: "reinvoke_with_platform_and_capability_id",
900
+ },
901
+ };
902
+ }
903
+ if (!deps.connectorExecutor || !deps.state) {
904
+ return {
905
+ ok: false,
906
+ command: "connector:run",
907
+ error: {
908
+ code: "MANUAL_RUN_DEPS_UNAVAILABLE",
909
+ message: "connector:run requires connectorExecutor and state database",
910
+ nextStep: "wire_connector_executor_and_state_into_ops_router",
911
+ },
912
+ };
913
+ }
914
+ const toolExperienceStore = createToolExperienceStore(deps.state);
915
+ const experienceWriter = createExperienceWriter(toolExperienceStore);
916
+ const wetProbeRunner = createWetProbeRunner();
917
+ const registryV7 = new CapabilityContractRegistryV7();
918
+ // Populate V7 registry from dynamic registry if available (best-effort)
919
+ if (deps.registry) {
920
+ for (const entry of deps.registry.listConnectors()) {
921
+ if (entry.manifestPath) {
922
+ try {
923
+ const manifestText = fs.readFileSync(entry.manifestPath, "utf-8");
924
+ const manifest = JSON.parse(manifestText);
925
+ registryV7.register(manifest);
926
+ }
927
+ catch {
928
+ // Skip manifests that can't be read or don't validate as V7
929
+ }
778
930
  }
779
931
  }
780
932
  }
933
+ const dispatcher = createManualRunDispatcher({
934
+ connectorExecutor: deps.connectorExecutor,
935
+ experienceWriter,
936
+ wetProbeRunner,
937
+ registryV7,
938
+ auditStore: deps.auditStore,
939
+ state: deps.state,
940
+ workspaceRoot: typeof input?.workspaceRoot === "string" ? input.workspaceRoot : process.cwd(),
941
+ });
942
+ return dispatcher.runConnector({
943
+ platformId,
944
+ capabilityId,
945
+ payload: typeof input?.payload === "object" && input?.payload !== null
946
+ ? input.payload
947
+ : undefined,
948
+ caller: typeof input?.caller === "string" ? input.caller : undefined,
949
+ reason: typeof input?.reason === "string" ? input.reason : undefined,
950
+ });
781
951
  }
782
- const dispatcher = createManualRunDispatcher({
783
- connectorExecutor: deps.connectorExecutor,
784
- experienceWriter,
785
- wetProbeRunner,
786
- registryV7,
787
- auditStore: deps.auditStore,
788
- state: deps.state,
789
- workspaceRoot: typeof input?.workspaceRoot === "string" ? input.workspaceRoot : process.cwd(),
790
- });
791
- return dispatcher.runConnector({
792
- platformId,
793
- capabilityId,
794
- payload: typeof input?.payload === "object" && input?.payload !== null
795
- ? input.payload
796
- : undefined,
797
- caller: typeof input?.caller === "string" ? input.caller : undefined,
798
- reason: typeof input?.reason === "string" ? input.reason : undefined,
799
- });
800
- }
801
- if (command === "goal") {
802
- const rawAction = typeof input?.action === "string" ? input.action : "list";
803
- const action = ["set", "list", "accept", "reject"].includes(rawAction)
804
- ? rawAction
805
- : "list";
806
- const sanitizeText = (v, maxLen = 1000) => {
807
- if (typeof v !== "string")
808
- return undefined;
809
- const trimmed = v.trim();
810
- if (trimmed.length === 0)
811
- return undefined;
812
- return trimmed.slice(0, maxLen);
813
- };
814
- return goalCommand(deps.state, {
815
- action,
816
- goalId: typeof input?.goalId === "string" ? input.goalId.trim().slice(0, 128) : undefined,
817
- description: sanitizeText(input?.description),
818
- completionCriteria: sanitizeText(input?.completionCriteria),
819
- // T1.4.2: criteria alias for completionCriteria
820
- criteria: sanitizeText(input?.criteria),
821
- risk: typeof input?.risk === "string"
822
- ? input.risk
823
- : undefined,
824
- kind: typeof input?.kind === "string"
825
- ? input.kind
826
- : undefined,
827
- statusFilter: typeof input?.statusFilter === "string" ? input.statusFilter : undefined,
828
- originFilter: typeof input?.originFilter === "string" ? input.originFilter : undefined,
829
- limit: typeof input?.limit === "number" ? input.limit : undefined,
830
- });
831
- }
832
- if (command === "dream:recent") {
833
- if (!deps.readModels) {
834
- return {
835
- ok: false,
836
- error: {
837
- code: "READ_MODELS_UNAVAILABLE",
838
- message: "dream:recent requires workspace read models",
839
- nextStep: "wire_read_models_into_ops_router",
840
- },
841
- };
842
- }
843
- const limit = typeof input?.limit === "number" ? input.limit : 5;
844
- const data = await deps.readModels.loadDreamRecent(limit);
845
- return { ok: true, data };
846
- }
847
- if (command === "cycle:recent") {
848
- if (!deps.readModels) {
849
- return {
850
- ok: false,
851
- error: {
852
- code: "READ_MODELS_UNAVAILABLE",
853
- message: "cycle:recent requires workspace read models",
854
- nextStep: "wire_read_models_into_ops_router",
855
- },
856
- };
857
- }
858
- const limit = typeof input?.limit === "number" ? input.limit : 5;
859
- const data = await deps.readModels.loadCycleRecent(limit);
860
- return { ok: true, data };
861
- }
862
- // ─── v8 commands (T-ROS.C.1) ─────────────────────────────────────────
863
- /**
864
- * [G1] loop_status — v8 causal loop health read model.
865
- * Returns machine-readable overallStatus, stalledAt, stageSummaries,
866
- * and human-readable nextAction for operator diagnosis.
867
- */
868
- if (command === "loop_status") {
869
- const generatedAt = new Date().toISOString();
870
- if (!deps.state) {
871
- const envelope = {
872
- ok: false,
873
- command: "loop_status",
874
- runtimeMode: "unavailable",
875
- surfaceMode: "cli",
876
- generatedAt,
877
- error: {
878
- code: "STATE_DB_UNAVAILABLE",
879
- message: "loop_status requires state database in OpsRouterDeps",
880
- nextStep: "wire_state_db_into_ops_router",
881
- },
882
- warnings: [],
883
- sourceRefs: [],
952
+ if (command === "goal") {
953
+ const rawAction = typeof input?.action === "string" ? input.action : "list";
954
+ const action = ["set", "list", "accept", "reject"].includes(rawAction)
955
+ ? rawAction
956
+ : "list";
957
+ const sanitizeText = (v, maxLen = 1000) => {
958
+ if (typeof v !== "string")
959
+ return undefined;
960
+ const trimmed = v.trim();
961
+ if (trimmed.length === 0)
962
+ return undefined;
963
+ return trimmed.slice(0, maxLen);
884
964
  };
885
- return envelope;
965
+ return goalCommand(deps.state, {
966
+ action,
967
+ goalId: typeof input?.goalId === "string" ? input.goalId.trim().slice(0, 128) : undefined,
968
+ description: sanitizeText(input?.description),
969
+ completionCriteria: sanitizeText(input?.completionCriteria),
970
+ // T1.4.2: criteria alias for completionCriteria
971
+ criteria: sanitizeText(input?.criteria),
972
+ risk: typeof input?.risk === "string"
973
+ ? input.risk
974
+ : undefined,
975
+ kind: typeof input?.kind === "string"
976
+ ? input.kind
977
+ : undefined,
978
+ statusFilter: typeof input?.statusFilter === "string" ? input.statusFilter : undefined,
979
+ originFilter: typeof input?.originFilter === "string" ? input.originFilter : undefined,
980
+ limit: typeof input?.limit === "number" ? input.limit : undefined,
981
+ });
886
982
  }
887
- try {
888
- const result = await readLoopStatus(deps.state);
889
- if (!result.ok) {
983
+ // ─── v8 commands (T-ROS.C.1) ─────────────────────────────────────────
984
+ /**
985
+ * [G1] loop_status — v8 causal loop health read model.
986
+ * Returns machine-readable overallStatus, stalledAt, stageSummaries,
987
+ * and human-readable nextAction for operator diagnosis.
988
+ */
989
+ if (command === "loop_status") {
990
+ const generatedAt = new Date().toISOString();
991
+ if (!deps.state) {
890
992
  const envelope = {
891
993
  ok: false,
892
994
  command: "loop_status",
893
- runtimeMode: "workspace_full_runtime",
995
+ runtimeMode: "unavailable",
894
996
  surfaceMode: "cli",
895
997
  generatedAt,
896
998
  error: {
897
- code: "LOOP_STATUS_DEGRADED",
898
- message: result.degraded.operatorNextAction,
899
- nextStep: "check_state_db_and_retry",
999
+ code: "STATE_DB_UNAVAILABLE",
1000
+ message: "loop_status requires state database in OpsRouterDeps",
1001
+ nextStep: "wire_state_db_into_ops_router",
900
1002
  },
901
- warnings: [result.degraded.reason],
902
- sourceRefs: result.degraded.sourceRefs.map((r) => r.uri),
1003
+ warnings: [],
1004
+ sourceRefs: [],
903
1005
  };
904
1006
  return envelope;
905
1007
  }
906
- const envelope = {
907
- ok: true,
908
- command: "loop_status",
909
- runtimeMode: "workspace_full_runtime",
910
- surfaceMode: "cli",
911
- generatedAt,
912
- data: result.status,
913
- warnings: [],
914
- sourceRefs: [],
915
- };
916
- return envelope;
917
- }
918
- catch (err) {
919
- const msg = err instanceof Error ? err.message : String(err);
920
- const envelope = {
921
- ok: false,
922
- command: "loop_status",
923
- runtimeMode: "unavailable",
924
- surfaceMode: "cli",
925
- generatedAt,
926
- error: { code: "LOOP_STATUS_EXCEPTION", message: msg },
927
- warnings: [],
928
- sourceRefs: [],
929
- };
930
- return envelope;
931
- }
932
- }
933
- // ─── v7 commands (T-ROS.C.1) ─────────────────────────────────────────
934
- /** [G2] self_health — transparent pass-through from SelfHealthSnapshot (DR-042). */
935
- if (command === "self_health") {
936
- const generatedAt = new Date().toISOString();
937
- try {
938
- ensureMinimumProbes();
939
- const snap = await getSelfHealthSnapshot();
940
- const degraded_dimensions = Object.entries(snap.dimensions)
941
- .filter(([, d]) => d.status === "degraded")
942
- .map(([k]) => k);
943
- const envelope = {
944
- ok: true,
945
- command: "self_health",
946
- runtimeMode: "workspace_full_runtime",
947
- surfaceMode: "cli",
948
- generatedAt,
949
- data: {
950
- overall: snap.overall,
951
- generatedAt: snap.generatedAt,
952
- degraded_dimensions,
953
- dimensions: snap.dimensions,
954
- },
955
- warnings: [],
956
- sourceRefs: ["observability/services/self-health-snapshot.ts"],
957
- };
958
- return envelope;
959
- }
960
- catch (err) {
961
- const msg = err instanceof Error ? err.message : String(err);
962
- const envelope = {
963
- ok: false,
964
- command: "self_health",
965
- runtimeMode: "unavailable",
966
- surfaceMode: "cli",
967
- generatedAt,
968
- error: { code: "SELF_HEALTH_PROBE_FAILED", message: msg },
969
- warnings: [],
970
- sourceRefs: [],
971
- };
972
- return envelope;
973
- }
974
- }
975
- /**
976
- * [G3] tool_affordance — body-tool AffordanceMap pass-through.
977
- * Port not yet wired in this wave; returns degraded view with clear next-step.
978
- */
979
- if (command === "tool_affordance") {
980
- const generatedAt = new Date().toISOString();
981
- if (deps.toolAffordancePort) {
982
- const allStatuses = [
983
- "safe",
984
- "exploratory",
985
- "needs_auth",
986
- "painful",
987
- "unavailable",
988
- ];
989
- const platformIds = Array.isArray(input?.platformIds)
990
- ? input.platformIds.filter((item) => typeof item === "string")
991
- : typeof input?.platformId === "string"
992
- ? [input.platformId]
993
- : undefined;
994
- const data = await deps.toolAffordancePort.assembleAffordanceMap({
995
- platformIds,
996
- allowedStatuses: allStatuses,
997
- goalKind: typeof input?.goalKind === "string" ? input.goalKind : undefined,
998
- });
999
- const envelope = {
1000
- ok: true,
1001
- command: "tool_affordance",
1002
- runtimeMode: "workspace_full_runtime",
1003
- surfaceMode: "cli",
1004
- generatedAt,
1005
- data,
1006
- warnings: [],
1007
- sourceRefs: [
1008
- "core/second-nature/body/tool-affordance/affordance-assembler.ts",
1009
- ],
1010
- };
1011
- return envelope;
1012
- }
1013
- const envelope = {
1014
- ok: false,
1015
- command: "tool_affordance",
1016
- runtimeMode: "unavailable",
1017
- surfaceMode: "cli",
1018
- generatedAt,
1019
- error: {
1020
- code: "TOOL_AFFORDANCE_PORT_UNWIRED",
1021
- message: "tool_affordance requires body-tool AffordanceMap port (T-BTS.C.1) to be wired into OpsRouterDeps",
1022
- nextStep: "wire_body_tool_port_into_ops_router_deps",
1023
- },
1024
- warnings: [],
1025
- sourceRefs: [],
1026
- };
1027
- return envelope;
1028
- }
1029
- /**
1030
- * [G6] heartbeat_digest — wraps generateHeartbeatDigest.
1031
- * Requires auditStore in deps; degrades if unavailable.
1032
- */
1033
- if (command === "heartbeat_digest") {
1034
- const generatedAt = new Date().toISOString();
1035
- if (!deps.auditStore) {
1036
- const envelope = {
1037
- ok: false,
1038
- command: "heartbeat_digest",
1039
- runtimeMode: "unavailable",
1040
- surfaceMode: "cli",
1041
- generatedAt,
1042
- error: {
1043
- code: "AUDIT_STORE_UNAVAILABLE",
1044
- message: "heartbeat_digest requires auditStore in OpsRouterDeps",
1045
- nextStep: "wire_audit_store_into_ops_router",
1046
- },
1047
- warnings: [],
1048
- sourceRefs: [],
1049
- };
1050
- return envelope;
1051
- }
1052
- const date = typeof input?.date === "string" && input.date
1053
- ? input.date
1054
- : new Date().toISOString().slice(0, 10);
1055
- try {
1056
- const digestDeps = {
1057
- auditStore: deps.auditStore,
1058
- ...deps.heartbeatDigestDeps,
1059
- };
1060
- const digest = await generateHeartbeatDigest(date, digestDeps);
1061
- // T-OBS.R.3: Embed real-run health into digest when state DB is available
1062
- if (deps.state) {
1063
- const realRunResult = await checkRealRunHealth(deps.state, date);
1064
- if (realRunResult.ok) {
1065
- digest.realRunHealth = {
1066
- gatePassed: realRunResult.gate.gatePassed,
1067
- contractSmokeOnly: realRunResult.gate.contractSmokeOnly,
1068
- seededStateDetected: realRunResult.gate.seededStateDetected,
1069
- hasRealClosure: realRunResult.gate.hasRealClosure,
1070
- hasQuietArtifact: realRunResult.gate.hasQuietArtifact,
1071
- hasDreamArtifact: realRunResult.gate.hasDreamArtifact,
1072
- hasFreshImpulseContext: realRunResult.gate.hasFreshImpulseContext,
1073
- hasProjectionFeedback: realRunResult.gate.hasProjectionFeedback,
1074
- missingStage: realRunResult.gate.missingStage,
1075
- missingReason: realRunResult.gate.missingReason,
1076
- };
1077
- }
1078
- else {
1079
- digest.realRunHealth = {
1080
- gatePassed: false,
1081
- contractSmokeOnly: false,
1082
- seededStateDetected: false,
1083
- hasRealClosure: false,
1084
- hasQuietArtifact: false,
1085
- hasDreamArtifact: false,
1086
- hasFreshImpulseContext: false,
1087
- hasProjectionFeedback: false,
1088
- missingReason: "Real-run health check degraded: " + realRunResult.degraded.reason,
1008
+ try {
1009
+ const result = await readLoopStatus(deps.state);
1010
+ if (!result.ok) {
1011
+ const envelope = {
1012
+ ok: false,
1013
+ command: "loop_status",
1014
+ runtimeMode: "workspace_full_runtime",
1015
+ surfaceMode: "cli",
1016
+ generatedAt,
1017
+ error: {
1018
+ code: "LOOP_STATUS_DEGRADED",
1019
+ message: result.degraded.operatorNextAction,
1020
+ nextStep: "check_state_db_and_retry",
1021
+ },
1022
+ warnings: [result.degraded.reason],
1023
+ sourceRefs: result.degraded.sourceRefs.map((r) => r.uri),
1089
1024
  };
1025
+ return envelope;
1090
1026
  }
1027
+ const envelope = {
1028
+ ok: true,
1029
+ command: "loop_status",
1030
+ runtimeMode: "workspace_full_runtime",
1031
+ surfaceMode: "cli",
1032
+ generatedAt,
1033
+ data: result.status,
1034
+ warnings: [],
1035
+ sourceRefs: [],
1036
+ evidenceLevel: result.status.evidenceLevel,
1037
+ };
1038
+ return envelope;
1039
+ }
1040
+ catch (err) {
1041
+ const msg = err instanceof Error ? err.message : String(err);
1042
+ const envelope = {
1043
+ ok: false,
1044
+ command: "loop_status",
1045
+ runtimeMode: "unavailable",
1046
+ surfaceMode: "cli",
1047
+ generatedAt,
1048
+ error: { code: "LOOP_STATUS_EXCEPTION", message: msg },
1049
+ warnings: [],
1050
+ sourceRefs: [],
1051
+ };
1052
+ return envelope;
1091
1053
  }
1092
- const envelope = {
1093
- ok: true,
1094
- command: "heartbeat_digest",
1095
- runtimeMode: "workspace_full_runtime",
1096
- surfaceMode: "cli",
1097
- generatedAt,
1098
- data: digest,
1099
- warnings: [],
1100
- sourceRefs: ["observability/services/heartbeat-digest-assembler.ts"],
1101
- };
1102
- return envelope;
1103
1054
  }
1104
- catch (err) {
1105
- const msg = err instanceof Error ? err.message : String(err);
1106
- const envelope = {
1107
- ok: false,
1108
- command: "heartbeat_digest",
1109
- runtimeMode: "unavailable",
1110
- surfaceMode: "cli",
1111
- generatedAt,
1112
- error: { code: "DIGEST_GENERATION_FAILED", message: msg },
1113
- warnings: [],
1114
- sourceRefs: [],
1115
- };
1116
- return envelope;
1055
+ // ─── v7 commands (T-ROS.C.1) ─────────────────────────────────────────
1056
+ /** [G2] self_health transparent pass-through from SelfHealthSnapshot (DR-042). */
1057
+ if (command === "self_health") {
1058
+ const generatedAt = new Date().toISOString();
1059
+ try {
1060
+ ensureMinimumProbes();
1061
+ const snap = await getSelfHealthSnapshot();
1062
+ const degraded_dimensions = Object.entries(snap.dimensions)
1063
+ .filter(([, d]) => d.status === "degraded")
1064
+ .map(([k]) => k);
1065
+ const envelope = {
1066
+ ok: true,
1067
+ command: "self_health",
1068
+ runtimeMode: "workspace_full_runtime",
1069
+ surfaceMode: "cli",
1070
+ generatedAt,
1071
+ data: {
1072
+ overall: snap.overall,
1073
+ generatedAt: snap.generatedAt,
1074
+ degraded_dimensions,
1075
+ dimensions: snap.dimensions,
1076
+ },
1077
+ warnings: [],
1078
+ sourceRefs: ["observability/services/self-health-snapshot.ts"],
1079
+ };
1080
+ return envelope;
1081
+ }
1082
+ catch (err) {
1083
+ const msg = err instanceof Error ? err.message : String(err);
1084
+ const envelope = {
1085
+ ok: false,
1086
+ command: "self_health",
1087
+ runtimeMode: "unavailable",
1088
+ surfaceMode: "cli",
1089
+ generatedAt,
1090
+ error: { code: "SELF_HEALTH_PROBE_FAILED", message: msg },
1091
+ warnings: [],
1092
+ sourceRefs: [],
1093
+ };
1094
+ return envelope;
1095
+ }
1117
1096
  }
1118
- }
1119
- /**
1120
- * [G6] snapshot:capture production capture path for RestoreSnapshot +
1121
- * NarrativeTimeline. This gives restore and narrative:diff real state to consume.
1122
- */
1123
- if (command === "snapshot:capture") {
1124
- return captureRuntimeSnapshot(deps, input);
1125
- }
1126
- /**
1127
- * [G6] narrative:diff — queryNarrativeDiff between two versions.
1128
- * Requires narrativeTimelineDeps in OpsRouterDeps.
1129
- */
1130
- if (command === "narrative:diff") {
1131
- const generatedAt = new Date().toISOString();
1132
- if (!deps.narrativeTimelineDeps) {
1097
+ /**
1098
+ * [G3] tool_affordance — body-tool AffordanceMap pass-through.
1099
+ * Port not yet wired in this wave; returns degraded view with clear next-step.
1100
+ */
1101
+ if (command === "tool_affordance") {
1102
+ const generatedAt = new Date().toISOString();
1103
+ if (deps.toolAffordancePort) {
1104
+ const allStatuses = [
1105
+ "safe",
1106
+ "exploratory",
1107
+ "needs_auth",
1108
+ "painful",
1109
+ "unavailable",
1110
+ ];
1111
+ const platformIds = Array.isArray(input?.platformIds)
1112
+ ? input.platformIds.filter((item) => typeof item === "string")
1113
+ : typeof input?.platformId === "string"
1114
+ ? [input.platformId]
1115
+ : undefined;
1116
+ const data = await deps.toolAffordancePort.assembleAffordanceMap({
1117
+ platformIds,
1118
+ allowedStatuses: allStatuses,
1119
+ goalKind: typeof input?.goalKind === "string" ? input.goalKind : undefined,
1120
+ });
1121
+ const envelope = {
1122
+ ok: true,
1123
+ command: "tool_affordance",
1124
+ runtimeMode: "workspace_full_runtime",
1125
+ surfaceMode: "cli",
1126
+ generatedAt,
1127
+ data,
1128
+ warnings: [],
1129
+ sourceRefs: [
1130
+ "core/second-nature/body/tool-affordance/affordance-assembler.ts",
1131
+ ],
1132
+ };
1133
+ return envelope;
1134
+ }
1133
1135
  const envelope = {
1134
1136
  ok: false,
1135
- command: "narrative:diff",
1137
+ command: "tool_affordance",
1136
1138
  runtimeMode: "unavailable",
1137
1139
  surfaceMode: "cli",
1138
1140
  generatedAt,
1139
1141
  error: {
1140
- code: "NARRATIVE_TIMELINE_PORT_UNAVAILABLE",
1141
- message: "narrative:diff requires narrativeTimelineDeps in OpsRouterDeps",
1142
- nextStep: "wire_narrative_timeline_deps_into_ops_router",
1142
+ code: "TOOL_AFFORDANCE_PORT_UNWIRED",
1143
+ message: "tool_affordance requires body-tool AffordanceMap port (T-BTS.C.1) to be wired into OpsRouterDeps",
1144
+ nextStep: "wire_body_tool_port_into_ops_router_deps",
1143
1145
  },
1144
1146
  warnings: [],
1145
1147
  sourceRefs: [],
1146
1148
  };
1147
1149
  return envelope;
1148
1150
  }
1149
- let fromVersion = typeof input?.from === "string" ? input.from : "";
1150
- let toVersion = typeof input?.to === "string" ? input.to : "";
1151
- if (!fromVersion || !toVersion) {
1152
- // Auto-resolve the two most recent narrative timeline versions when not provided.
1153
- const recent = await deps.narrativeTimelineDeps.stateMemoryPort.listNarrativeTimeline(new Date(0).toISOString(), new Date().toISOString(), { limit: 2 });
1154
- if (recent.length < 2) {
1151
+ /**
1152
+ * [G6] heartbeat_digest wraps generateHeartbeatDigest.
1153
+ * Requires auditStore in deps; degrades if unavailable.
1154
+ */
1155
+ if (command === "heartbeat_digest") {
1156
+ const generatedAt = new Date().toISOString();
1157
+ if (!deps.auditStore) {
1155
1158
  const envelope = {
1156
1159
  ok: false,
1157
- command: "narrative:diff",
1158
- runtimeMode: "workspace_full_runtime",
1160
+ command: "heartbeat_digest",
1161
+ runtimeMode: "unavailable",
1159
1162
  surfaceMode: "cli",
1160
1163
  generatedAt,
1161
1164
  error: {
1162
- code: "NARRATIVE_DIFF_REQUIRES_TWO_VERSIONS",
1163
- message: `narrative:diff requires at least two timeline versions; found ${recent.length}. Pass explicit 'from' and 'to', or run snapshot:capture twice.`,
1164
- nextStep: "run_snapshot_capture_twice_or_pass_from_and_to",
1165
+ code: "AUDIT_STORE_UNAVAILABLE",
1166
+ message: "heartbeat_digest requires auditStore in OpsRouterDeps",
1167
+ nextStep: "wire_audit_store_into_ops_router",
1165
1168
  },
1166
1169
  warnings: [],
1167
1170
  sourceRefs: [],
1168
1171
  };
1169
1172
  return envelope;
1170
1173
  }
1171
- fromVersion = recent[1].version;
1172
- toVersion = recent[0].version;
1174
+ const date = typeof input?.date === "string" && input.date
1175
+ ? input.date
1176
+ : new Date().toISOString().slice(0, 10);
1177
+ try {
1178
+ const digestDeps = {
1179
+ auditStore: deps.auditStore,
1180
+ ...deps.heartbeatDigestDeps,
1181
+ };
1182
+ const digest = await generateHeartbeatDigest(date, digestDeps);
1183
+ // T-OBS.R.3: Embed real-run health into digest when state DB is available
1184
+ if (deps.state) {
1185
+ const realRunResult = await checkRealRunHealth(deps.state, date);
1186
+ if (realRunResult.ok) {
1187
+ digest.realRunHealth = {
1188
+ gatePassed: realRunResult.gate.gatePassed,
1189
+ contractSmokeOnly: realRunResult.gate.contractSmokeOnly,
1190
+ seededStateDetected: realRunResult.gate.seededStateDetected,
1191
+ hasRealClosure: realRunResult.gate.hasRealClosure,
1192
+ hasQuietArtifact: realRunResult.gate.hasQuietArtifact,
1193
+ hasDreamArtifact: realRunResult.gate.hasDreamArtifact,
1194
+ hasFreshImpulseContext: realRunResult.gate.hasFreshImpulseContext,
1195
+ hasProjectionFeedback: realRunResult.gate.hasProjectionFeedback,
1196
+ missingStage: realRunResult.gate.missingStage,
1197
+ missingReason: realRunResult.gate.missingReason,
1198
+ };
1199
+ }
1200
+ else {
1201
+ digest.realRunHealth = {
1202
+ gatePassed: false,
1203
+ contractSmokeOnly: false,
1204
+ seededStateDetected: false,
1205
+ hasRealClosure: false,
1206
+ hasQuietArtifact: false,
1207
+ hasDreamArtifact: false,
1208
+ hasFreshImpulseContext: false,
1209
+ hasProjectionFeedback: false,
1210
+ missingReason: "Real-run health check degraded: " + realRunResult.degraded.reason,
1211
+ };
1212
+ }
1213
+ }
1214
+ const envelope = {
1215
+ ok: true,
1216
+ command: "heartbeat_digest",
1217
+ runtimeMode: "workspace_full_runtime",
1218
+ surfaceMode: "cli",
1219
+ generatedAt,
1220
+ data: digest,
1221
+ warnings: [],
1222
+ sourceRefs: ["observability/services/heartbeat-digest-assembler.ts"],
1223
+ };
1224
+ return envelope;
1225
+ }
1226
+ catch (err) {
1227
+ const msg = err instanceof Error ? err.message : String(err);
1228
+ const envelope = {
1229
+ ok: false,
1230
+ command: "heartbeat_digest",
1231
+ runtimeMode: "unavailable",
1232
+ surfaceMode: "cli",
1233
+ generatedAt,
1234
+ error: { code: "DIGEST_GENERATION_FAILED", message: msg },
1235
+ warnings: [],
1236
+ sourceRefs: [],
1237
+ };
1238
+ return envelope;
1239
+ }
1173
1240
  }
1174
- try {
1175
- const diff = await queryNarrativeDiff(fromVersion, toVersion, deps.narrativeTimelineDeps);
1176
- const envelope = {
1177
- ok: true,
1178
- command: "narrative:diff",
1179
- runtimeMode: "workspace_full_runtime",
1180
- surfaceMode: "cli",
1181
- generatedAt,
1182
- data: diff,
1183
- warnings: [],
1184
- sourceRefs: ["observability/services/narrative-timeline-query-service.ts"],
1185
- };
1186
- return envelope;
1241
+ /**
1242
+ * [G6] snapshot:capture production capture path for RestoreSnapshot +
1243
+ * NarrativeTimeline. This gives restore and narrative:diff real state to consume.
1244
+ */
1245
+ if (command === "snapshot:capture") {
1246
+ return captureRuntimeSnapshot(deps, input);
1187
1247
  }
1188
- catch (err) {
1189
- if (err instanceof NarrativeVersionNotFoundError) {
1248
+ /**
1249
+ * [G6] narrative:diff queryNarrativeDiff between two versions.
1250
+ * Requires narrativeTimelineDeps in OpsRouterDeps.
1251
+ */
1252
+ if (command === "narrative:diff") {
1253
+ const generatedAt = new Date().toISOString();
1254
+ if (!deps.narrativeTimelineDeps) {
1190
1255
  const envelope = {
1191
1256
  ok: false,
1192
1257
  command: "narrative:diff",
1193
- runtimeMode: "workspace_full_runtime",
1258
+ runtimeMode: "unavailable",
1194
1259
  surfaceMode: "cli",
1195
1260
  generatedAt,
1196
1261
  error: {
1197
- code: "NARRATIVE_VERSION_NOT_FOUND",
1198
- message: err.message,
1199
- nextStep: "verify_version_exists_in_timeline",
1262
+ code: "NARRATIVE_TIMELINE_PORT_UNAVAILABLE",
1263
+ message: "narrative:diff requires narrativeTimelineDeps in OpsRouterDeps",
1264
+ nextStep: "wire_narrative_timeline_deps_into_ops_router",
1200
1265
  },
1201
1266
  warnings: [],
1202
1267
  sourceRefs: [],
1203
1268
  };
1204
1269
  return envelope;
1205
1270
  }
1206
- const msg = err instanceof Error ? err.message : String(err);
1207
- const envelope = {
1208
- ok: false,
1209
- command: "narrative:diff",
1210
- runtimeMode: "unavailable",
1211
- surfaceMode: "cli",
1212
- generatedAt,
1213
- error: { code: "NARRATIVE_DIFF_FAILED", message: msg },
1214
- warnings: [],
1215
- sourceRefs: [],
1216
- };
1217
- return envelope;
1218
- }
1219
- }
1220
- /**
1221
- * [G6] timeline — queryNarrativeTimeline with cursor pagination.
1222
- * Requires narrativeTimelineDeps in OpsRouterDeps.
1223
- */
1224
- if (command === "timeline") {
1225
- const generatedAt = new Date().toISOString();
1226
- if (!deps.narrativeTimelineDeps) {
1227
- const envelope = {
1228
- ok: false,
1229
- command: "timeline",
1230
- runtimeMode: "unavailable",
1231
- surfaceMode: "cli",
1232
- generatedAt,
1233
- error: {
1234
- code: "NARRATIVE_TIMELINE_PORT_UNAVAILABLE",
1235
- message: "timeline requires narrativeTimelineDeps in OpsRouterDeps",
1236
- nextStep: "wire_narrative_timeline_deps_into_ops_router",
1237
- },
1238
- warnings: [],
1239
- sourceRefs: [],
1240
- };
1241
- return envelope;
1242
- }
1243
- const now = new Date();
1244
- const to = typeof input?.to === "string" ? input.to : now.toISOString();
1245
- const from = typeof input?.from === "string"
1246
- ? input.from
1247
- : new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000).toISOString();
1248
- const limit = typeof input?.limit === "number" ? input.limit : 20;
1249
- const cursor = typeof input?.cursor === "string" ? input.cursor : undefined;
1250
- try {
1251
- const page = await queryNarrativeTimeline(from, to, { limit, cursor }, deps.narrativeTimelineDeps);
1252
- const envelope = {
1253
- ok: true,
1254
- command: "timeline",
1255
- runtimeMode: "workspace_full_runtime",
1256
- surfaceMode: "cli",
1257
- generatedAt,
1258
- data: page,
1259
- warnings: [],
1260
- sourceRefs: ["observability/services/narrative-timeline-query-service.ts"],
1261
- };
1262
- return envelope;
1263
- }
1264
- catch (err) {
1265
- const msg = err instanceof Error ? err.message : String(err);
1266
- const code = err.name === "NarrativeQueryRangeError"
1267
- ? "NARRATIVE_RANGE_EXCEEDED"
1268
- : "TIMELINE_QUERY_FAILED";
1269
- const envelope = {
1270
- ok: false,
1271
- command: "timeline",
1272
- runtimeMode: "unavailable",
1273
- surfaceMode: "cli",
1274
- generatedAt,
1275
- error: { code, message: msg },
1276
- warnings: [],
1277
- sourceRefs: [],
1278
- };
1279
- return envelope;
1280
- }
1281
- }
1282
- /**
1283
- * [G6] restore — bounded state restoration via RestoreSnapshotStore + audit (T-ROS.C.1, T-OBS.C.6).
1284
- * When restoreSnapshotStore is wired, attempts to apply the snapshot payload back to state.
1285
- * Always writes RestoreAudit. Never restores credential fields.
1286
- */
1287
- if (command === "restore") {
1288
- const generatedAt = new Date().toISOString();
1289
- if (!deps.auditStore) {
1290
- const envelope = {
1291
- ok: false,
1292
- command: "restore",
1293
- runtimeMode: "unavailable",
1294
- surfaceMode: "cli",
1295
- generatedAt,
1296
- error: {
1297
- code: "AUDIT_STORE_UNAVAILABLE",
1298
- message: "restore requires auditStore in OpsRouterDeps",
1299
- nextStep: "wire_audit_store_into_ops_router",
1300
- },
1301
- warnings: [],
1302
- sourceRefs: [],
1303
- };
1304
- return envelope;
1271
+ let fromVersion = typeof input?.from === "string" ? input.from : "";
1272
+ let toVersion = typeof input?.to === "string" ? input.to : "";
1273
+ if (!fromVersion || !toVersion) {
1274
+ // Auto-resolve the two most recent narrative timeline versions when not provided.
1275
+ const recent = await deps.narrativeTimelineDeps.stateMemoryPort.listNarrativeTimeline(new Date(0).toISOString(), new Date().toISOString(), { limit: 2 });
1276
+ if (recent.length < 2) {
1277
+ const envelope = {
1278
+ ok: false,
1279
+ command: "narrative:diff",
1280
+ runtimeMode: "workspace_full_runtime",
1281
+ surfaceMode: "cli",
1282
+ generatedAt,
1283
+ error: {
1284
+ code: "NARRATIVE_DIFF_REQUIRES_TWO_VERSIONS",
1285
+ message: `narrative:diff requires at least two timeline versions; found ${recent.length}. Pass explicit 'from' and 'to', or run snapshot:capture twice.`,
1286
+ nextStep: "run_snapshot_capture_twice_or_pass_from_and_to",
1287
+ },
1288
+ warnings: [],
1289
+ sourceRefs: [],
1290
+ };
1291
+ return envelope;
1292
+ }
1293
+ fromVersion = recent[1].version;
1294
+ toVersion = recent[0].version;
1295
+ }
1296
+ try {
1297
+ const diff = await queryNarrativeDiff(fromVersion, toVersion, deps.narrativeTimelineDeps);
1298
+ const envelope = {
1299
+ ok: true,
1300
+ command: "narrative:diff",
1301
+ runtimeMode: "workspace_full_runtime",
1302
+ surfaceMode: "cli",
1303
+ generatedAt,
1304
+ data: diff,
1305
+ warnings: [],
1306
+ sourceRefs: ["observability/services/narrative-timeline-query-service.ts"],
1307
+ };
1308
+ return envelope;
1309
+ }
1310
+ catch (err) {
1311
+ if (err instanceof NarrativeVersionNotFoundError) {
1312
+ const envelope = {
1313
+ ok: false,
1314
+ command: "narrative:diff",
1315
+ runtimeMode: "workspace_full_runtime",
1316
+ surfaceMode: "cli",
1317
+ generatedAt,
1318
+ error: {
1319
+ code: "NARRATIVE_VERSION_NOT_FOUND",
1320
+ message: err.message,
1321
+ nextStep: "verify_version_exists_in_timeline",
1322
+ },
1323
+ warnings: [],
1324
+ sourceRefs: [],
1325
+ };
1326
+ return envelope;
1327
+ }
1328
+ const msg = err instanceof Error ? err.message : String(err);
1329
+ const envelope = {
1330
+ ok: false,
1331
+ command: "narrative:diff",
1332
+ runtimeMode: "unavailable",
1333
+ surfaceMode: "cli",
1334
+ generatedAt,
1335
+ error: { code: "NARRATIVE_DIFF_FAILED", message: msg },
1336
+ warnings: [],
1337
+ sourceRefs: [],
1338
+ };
1339
+ return envelope;
1340
+ }
1305
1341
  }
1306
- let restoreTarget;
1307
- let fromVersion;
1308
- let toVersion;
1309
- // T-V7C.C.5: snapshotId operator-friendly parameter takes precedence over legacy fields.
1310
- // When snapshotId is provided, resolve restoreTarget/fromVersion/toVersion from the
1311
- // matching snapshot row; otherwise fall back to explicit legacy parameters.
1312
- const snapshotId = textInput(input, "snapshotId");
1313
- if (snapshotId) {
1314
- if (!deps.restoreSnapshotStore) {
1342
+ /**
1343
+ * [G6] timeline — queryNarrativeTimeline with cursor pagination.
1344
+ * Requires narrativeTimelineDeps in OpsRouterDeps.
1345
+ */
1346
+ if (command === "timeline") {
1347
+ const generatedAt = new Date().toISOString();
1348
+ if (!deps.narrativeTimelineDeps) {
1315
1349
  const envelope = {
1316
1350
  ok: false,
1317
- command: "restore",
1351
+ command: "timeline",
1318
1352
  runtimeMode: "unavailable",
1319
1353
  surfaceMode: "cli",
1320
1354
  generatedAt,
1321
1355
  error: {
1322
- code: "RESTORE_SNAPSHOT_STORE_UNAVAILABLE",
1323
- message: "snapshotId restore requires restoreSnapshotStore in OpsRouterDeps",
1324
- nextStep: "wire_restore_snapshot_store_into_ops_router",
1356
+ code: "NARRATIVE_TIMELINE_PORT_UNAVAILABLE",
1357
+ message: "timeline requires narrativeTimelineDeps in OpsRouterDeps",
1358
+ nextStep: "wire_narrative_timeline_deps_into_ops_router",
1325
1359
  },
1326
1360
  warnings: [],
1327
1361
  sourceRefs: [],
1328
1362
  };
1329
1363
  return envelope;
1330
1364
  }
1331
- const snapshots = await deps.restoreSnapshotStore.listSnapshots();
1332
- const match = snapshots.find((s) => s.snapshotId === snapshotId);
1333
- if (match) {
1334
- restoreTarget = snapshotId;
1335
- fromVersion = match.capturedAt;
1336
- toVersion = snapshotId;
1365
+ const now = new Date();
1366
+ const to = typeof input?.to === "string" ? input.to : now.toISOString();
1367
+ const from = typeof input?.from === "string"
1368
+ ? input.from
1369
+ : new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000).toISOString();
1370
+ const limit = typeof input?.limit === "number" ? input.limit : 20;
1371
+ const cursor = typeof input?.cursor === "string" ? input.cursor : undefined;
1372
+ try {
1373
+ const page = await queryNarrativeTimeline(from, to, { limit, cursor }, deps.narrativeTimelineDeps);
1374
+ const envelope = {
1375
+ ok: true,
1376
+ command: "timeline",
1377
+ runtimeMode: "workspace_full_runtime",
1378
+ surfaceMode: "cli",
1379
+ generatedAt,
1380
+ data: page,
1381
+ warnings: [],
1382
+ sourceRefs: ["observability/services/narrative-timeline-query-service.ts"],
1383
+ };
1384
+ return envelope;
1337
1385
  }
1338
- else {
1386
+ catch (err) {
1387
+ const msg = err instanceof Error ? err.message : String(err);
1388
+ const code = err.name === "NarrativeQueryRangeError"
1389
+ ? "NARRATIVE_RANGE_EXCEEDED"
1390
+ : "TIMELINE_QUERY_FAILED";
1339
1391
  const envelope = {
1340
1392
  ok: false,
1341
- command: "restore",
1342
- runtimeMode: "workspace_full_runtime",
1393
+ command: "timeline",
1394
+ runtimeMode: "unavailable",
1343
1395
  surfaceMode: "cli",
1344
1396
  generatedAt,
1345
- error: {
1346
- code: "SNAPSHOT_NOT_FOUND",
1347
- message: `snapshotId ${snapshotId} not found in restore_snapshot table`,
1348
- nextStep: "list_available_snapshots_or_verify_snapshotId",
1349
- },
1397
+ error: { code, message: msg },
1350
1398
  warnings: [],
1351
1399
  sourceRefs: [],
1352
1400
  };
1353
1401
  return envelope;
1354
1402
  }
1355
1403
  }
1356
- else {
1357
- const missingFields = [];
1358
- if (typeof input?.restoreTarget !== "string")
1359
- missingFields.push("restoreTarget");
1360
- if (typeof input?.fromVersion !== "string")
1361
- missingFields.push("fromVersion");
1362
- if (typeof input?.toVersion !== "string")
1363
- missingFields.push("toVersion");
1364
- if (missingFields.length > 0) {
1404
+ /**
1405
+ * [G6] restore — bounded state restoration via RestoreSnapshotStore + audit (T-ROS.C.1, T-OBS.C.6).
1406
+ * When restoreSnapshotStore is wired, attempts to apply the snapshot payload back to state.
1407
+ * Always writes RestoreAudit. Never restores credential fields.
1408
+ */
1409
+ if (command === "restore") {
1410
+ const generatedAt = new Date().toISOString();
1411
+ if (!deps.auditStore) {
1365
1412
  const envelope = {
1366
1413
  ok: false,
1367
1414
  command: "restore",
1368
- runtimeMode: "workspace_full_runtime",
1415
+ runtimeMode: "unavailable",
1369
1416
  surfaceMode: "cli",
1370
1417
  generatedAt,
1371
1418
  error: {
1372
- code: "MISSING_RESTORE_FIELDS",
1373
- message: `restore requires: ${missingFields.join(", ")}`,
1374
- nextStep: "reinvoke_with_required_fields",
1419
+ code: "AUDIT_STORE_UNAVAILABLE",
1420
+ message: "restore requires auditStore in OpsRouterDeps",
1421
+ nextStep: "wire_audit_store_into_ops_router",
1375
1422
  },
1376
1423
  warnings: [],
1377
1424
  sourceRefs: [],
1378
1425
  };
1379
1426
  return envelope;
1380
1427
  }
1381
- restoreTarget = input.restoreTarget;
1382
- fromVersion = input.fromVersion;
1383
- toVersion = input.toVersion;
1384
- }
1385
- // [NEW] Invoke bounded restore via RestoreSnapshotStore when wired
1386
- let restoreResult = {
1387
- ok: false,
1388
- completedEntities: [],
1389
- failedEntities: [],
1390
- warnings: ["restore_snapshot_store_unavailable"],
1391
- };
1392
- if (deps.restoreSnapshotStore) {
1393
- restoreResult = await deps.restoreSnapshotStore.applyBoundedRestore({
1428
+ let restoreTarget;
1429
+ let fromVersion;
1430
+ let toVersion;
1431
+ // T-V7C.C.5: snapshotId operator-friendly parameter takes precedence over legacy fields.
1432
+ // When snapshotId is provided, resolve restoreTarget/fromVersion/toVersion from the
1433
+ // matching snapshot row; otherwise fall back to explicit legacy parameters.
1434
+ const snapshotId = textInput(input, "snapshotId");
1435
+ if (snapshotId) {
1436
+ if (!deps.restoreSnapshotStore) {
1437
+ const envelope = {
1438
+ ok: false,
1439
+ command: "restore",
1440
+ runtimeMode: "unavailable",
1441
+ surfaceMode: "cli",
1442
+ generatedAt,
1443
+ error: {
1444
+ code: "RESTORE_SNAPSHOT_STORE_UNAVAILABLE",
1445
+ message: "snapshotId restore requires restoreSnapshotStore in OpsRouterDeps",
1446
+ nextStep: "wire_restore_snapshot_store_into_ops_router",
1447
+ },
1448
+ warnings: [],
1449
+ sourceRefs: [],
1450
+ };
1451
+ return envelope;
1452
+ }
1453
+ const snapshots = await deps.restoreSnapshotStore.listSnapshots();
1454
+ const match = snapshots.find((s) => s.snapshotId === snapshotId);
1455
+ if (match) {
1456
+ restoreTarget = snapshotId;
1457
+ fromVersion = match.capturedAt;
1458
+ toVersion = snapshotId;
1459
+ }
1460
+ else {
1461
+ const envelope = {
1462
+ ok: false,
1463
+ command: "restore",
1464
+ runtimeMode: "workspace_full_runtime",
1465
+ surfaceMode: "cli",
1466
+ generatedAt,
1467
+ error: {
1468
+ code: "SNAPSHOT_NOT_FOUND",
1469
+ message: `snapshotId ${snapshotId} not found in restore_snapshot table`,
1470
+ nextStep: "list_available_snapshots_or_verify_snapshotId",
1471
+ },
1472
+ warnings: [],
1473
+ sourceRefs: [],
1474
+ };
1475
+ return envelope;
1476
+ }
1477
+ }
1478
+ else {
1479
+ const missingFields = [];
1480
+ if (typeof input?.restoreTarget !== "string")
1481
+ missingFields.push("restoreTarget");
1482
+ if (typeof input?.fromVersion !== "string")
1483
+ missingFields.push("fromVersion");
1484
+ if (typeof input?.toVersion !== "string")
1485
+ missingFields.push("toVersion");
1486
+ if (missingFields.length > 0) {
1487
+ const envelope = {
1488
+ ok: false,
1489
+ command: "restore",
1490
+ runtimeMode: "workspace_full_runtime",
1491
+ surfaceMode: "cli",
1492
+ generatedAt,
1493
+ error: {
1494
+ code: "MISSING_RESTORE_FIELDS",
1495
+ message: `restore requires: ${missingFields.join(", ")}`,
1496
+ nextStep: "reinvoke_with_required_fields",
1497
+ },
1498
+ warnings: [],
1499
+ sourceRefs: [],
1500
+ };
1501
+ return envelope;
1502
+ }
1503
+ restoreTarget = input.restoreTarget;
1504
+ fromVersion = input.fromVersion;
1505
+ toVersion = input.toVersion;
1506
+ }
1507
+ // [NEW] Invoke bounded restore via RestoreSnapshotStore when wired
1508
+ let restoreResult = {
1509
+ ok: false,
1510
+ completedEntities: [],
1511
+ failedEntities: [],
1512
+ warnings: ["restore_snapshot_store_unavailable"],
1513
+ };
1514
+ if (deps.restoreSnapshotStore) {
1515
+ restoreResult = await deps.restoreSnapshotStore.applyBoundedRestore({
1516
+ restoreTarget: restoreTarget,
1517
+ fromVersion: fromVersion,
1518
+ toVersion: toVersion,
1519
+ });
1520
+ }
1521
+ const event = {
1522
+ id: `restore-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
1394
1523
  restoreTarget: restoreTarget,
1395
1524
  fromVersion: fromVersion,
1396
1525
  toVersion: toVersion,
1397
- });
1398
- }
1399
- const event = {
1400
- id: `restore-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
1401
- restoreTarget: restoreTarget,
1402
- fromVersion: fromVersion,
1403
- toVersion: toVersion,
1404
- triggeredBy: input?.triggeredBy ?? "operator",
1405
- reason: typeof input?.reason === "string" ? input.reason : "manual_restore",
1406
- completedEntities: restoreResult.completedEntities,
1407
- failedEntities: restoreResult.failedEntities,
1408
- // credentials are always excluded from restore audit
1409
- excludedFields: Array.isArray(input?.excludedFields)
1410
- ? input.excludedFields.filter((f) => typeof f === "string")
1411
- : ["credential", "encryptionKey"],
1412
- restoredFieldCount: restoreResult.completedEntities.length,
1413
- createdAt: generatedAt,
1414
- traceId: typeof input?.traceId === "string" ? input.traceId : `trace-restore-${Date.now()}`,
1415
- };
1416
- const auditResult = await writeRestoreAudit(event, deps.auditStore);
1417
- const envelope = {
1418
- ok: restoreResult.ok && auditResult.ok,
1419
- command: "restore",
1420
- runtimeMode: "workspace_full_runtime",
1421
- surfaceMode: "cli",
1422
- generatedAt,
1423
- data: {
1424
- auditWritten: auditResult.warnings.length === 0,
1425
- fromVersion: event.fromVersion,
1426
- toVersion: event.toVersion,
1427
- restoreTarget: event.restoreTarget,
1428
- isPartialRestore: event.failedEntities.length > 0,
1429
- failedEntities: event.failedEntities,
1430
- completedEntities: event.completedEntities,
1431
- restoreSnapshotStoreAvailable: !!deps.restoreSnapshotStore,
1432
- },
1433
- warnings: [...restoreResult.warnings, ...auditResult.warnings],
1434
- sourceRefs: [
1435
- "observability/services/restore-audit-service.ts",
1436
- "storage/services/restore-snapshot-store.ts",
1437
- ],
1438
- };
1439
- return envelope;
1440
- }
1441
- /**
1442
- * [G7] runtime_secret_bootstrap — RuntimeSecretAnchorView pass-through.
1443
- * Requires secretAnchorDeps in OpsRouterDeps; never returns key plaintext.
1444
- */
1445
- if (command === "runtime_secret_bootstrap") {
1446
- const generatedAt = new Date().toISOString();
1447
- if (!deps.secretAnchorDeps) {
1448
- const envelope = {
1449
- ok: false,
1450
- command: "runtime_secret_bootstrap",
1451
- runtimeMode: "unavailable",
1452
- surfaceMode: "cli",
1453
- generatedAt,
1454
- error: {
1455
- code: "SECRET_ANCHOR_DEPS_UNAVAILABLE",
1456
- message: "runtime_secret_bootstrap requires secretAnchorDeps in OpsRouterDeps",
1457
- nextStep: "wire_secret_anchor_deps_into_ops_router",
1458
- },
1459
- warnings: [],
1460
- sourceRefs: [],
1461
- };
1462
- return envelope;
1463
- }
1464
- try {
1465
- const view = await viewSecretAnchor(deps.secretAnchorDeps);
1466
- // Map to RuntimeSecretBootstrapView (design model §6.1)
1467
- const data = {
1468
- status: view.status === "verified" || view.status === "ok"
1469
- ? "ok"
1470
- : view.status === "missing"
1471
- ? "runtime_secret_anchor_missing"
1472
- : view.status === "wrong_key"
1473
- ? "credential_recovery_required"
1474
- : view.status === "decryption_failed"
1475
- ? "runtime_secret_unavailable"
1476
- : "unknown",
1477
- keyHealth: view.status === "verified" || view.status === "ok"
1478
- ? "ok"
1479
- : view.status === "missing"
1480
- ? "missing_key"
1481
- : view.status === "wrong_key"
1482
- ? "wrong_key"
1483
- : "unknown",
1484
- anchorLocation: view.keyPath,
1485
- recoveryPrincipleRef: view.recoveryDocRef,
1486
- plaintextKeyExposed: false,
1487
- reasonCode: view.reasonCode,
1488
- recoverySteps: view.recoverySteps,
1526
+ triggeredBy: input?.triggeredBy ?? "operator",
1527
+ reason: typeof input?.reason === "string" ? input.reason : "manual_restore",
1528
+ completedEntities: restoreResult.completedEntities,
1529
+ failedEntities: restoreResult.failedEntities,
1530
+ // credentials are always excluded from restore audit
1531
+ excludedFields: Array.isArray(input?.excludedFields)
1532
+ ? input.excludedFields.filter((f) => typeof f === "string")
1533
+ : ["credential", "encryptionKey"],
1534
+ restoredFieldCount: restoreResult.completedEntities.length,
1535
+ createdAt: generatedAt,
1536
+ traceId: typeof input?.traceId === "string" ? input.traceId : `trace-restore-${Date.now()}`,
1489
1537
  };
1538
+ const auditResult = await writeRestoreAudit(event, deps.auditStore);
1490
1539
  const envelope = {
1491
- ok: true,
1492
- command: "runtime_secret_bootstrap",
1540
+ ok: restoreResult.ok && auditResult.ok,
1541
+ command: "restore",
1493
1542
  runtimeMode: "workspace_full_runtime",
1494
1543
  surfaceMode: "cli",
1495
1544
  generatedAt,
1496
- data,
1497
- warnings: [],
1498
- sourceRefs: ["observability/services/runtime-secret-anchor-view.ts"],
1499
- };
1500
- return envelope;
1501
- }
1502
- catch (err) {
1503
- const msg = err instanceof Error ? err.message : String(err);
1504
- const envelope = {
1505
- ok: false,
1506
- command: "runtime_secret_bootstrap",
1507
- runtimeMode: "unavailable",
1508
- surfaceMode: "cli",
1509
- generatedAt,
1510
- error: { code: "SECRET_ANCHOR_PROBE_FAILED", message: msg },
1511
- warnings: [],
1512
- sourceRefs: [],
1513
- };
1514
- return envelope;
1515
- }
1516
- }
1517
- // ─── T-V7C.C.4R + T-GVS.R.1: guidance_payload ─────────────────────────
1518
- // Returns the assembled impulse + atmosphere for a given scene context.
1519
- // When state DB is wired, reads persisted artifact first; falls back to
1520
- // real-time assembly and persists for subsequent reads.
1521
- if (command === "guidance_payload") {
1522
- const generatedAt = new Date().toISOString();
1523
- const sceneType = input?.sceneType ?? "social";
1524
- const capabilityIntent = typeof input?.capabilityIntent === "string"
1525
- ? input.capabilityIntent
1526
- : undefined;
1527
- const platformId = typeof input?.platformId === "string"
1528
- ? input.platformId
1529
- : undefined;
1530
- const validSceneTypes = ["social", "reply", "outreach", "quiet", "heartbeat", "explain", "user_reply"];
1531
- if (!validSceneTypes.includes(sceneType)) {
1532
- const envelope = {
1533
- ok: false,
1534
- command: "guidance_payload",
1535
- runtimeMode: "unavailable",
1536
- surfaceMode: "cli",
1537
- generatedAt,
1538
- error: {
1539
- code: "INVALID_SCENE_TYPE",
1540
- message: `sceneType must be one of: ${validSceneTypes.join(", ")}`,
1541
- nextStep: "reinvoke_with_valid_scene_type",
1545
+ data: {
1546
+ auditWritten: auditResult.warnings.length === 0,
1547
+ fromVersion: event.fromVersion,
1548
+ toVersion: event.toVersion,
1549
+ restoreTarget: event.restoreTarget,
1550
+ isPartialRestore: event.failedEntities.length > 0,
1551
+ failedEntities: event.failedEntities,
1552
+ completedEntities: event.completedEntities,
1553
+ restoreSnapshotStoreAvailable: !!deps.restoreSnapshotStore,
1542
1554
  },
1543
- warnings: [],
1544
- sourceRefs: [],
1555
+ warnings: [...restoreResult.warnings, ...auditResult.warnings],
1556
+ sourceRefs: [
1557
+ "observability/services/restore-audit-service.ts",
1558
+ "storage/services/restore-snapshot-store.ts",
1559
+ ],
1545
1560
  };
1546
1561
  return envelope;
1547
1562
  }
1548
- // T-GVS.R.1: Try reading persisted artifact first
1549
- let artifactData;
1550
- let warnings = [];
1551
- let sourceRefs = [
1552
- "guidance/capability-class.ts",
1553
- "guidance/impulse-assembler.ts",
1554
- "guidance/template-registry.ts",
1555
- "guidance/output-guard.ts",
1556
- ];
1557
- if (deps.state) {
1563
+ /**
1564
+ * [G7] runtime_secret_bootstrap — RuntimeSecretAnchorView pass-through.
1565
+ * Requires secretAnchorDeps in OpsRouterDeps; never returns key plaintext.
1566
+ */
1567
+ if (command === "runtime_secret_bootstrap") {
1568
+ const generatedAt = new Date().toISOString();
1569
+ if (!deps.secretAnchorDeps) {
1570
+ const envelope = {
1571
+ ok: false,
1572
+ command: "runtime_secret_bootstrap",
1573
+ runtimeMode: "unavailable",
1574
+ surfaceMode: "cli",
1575
+ generatedAt,
1576
+ error: {
1577
+ code: "SECRET_ANCHOR_DEPS_UNAVAILABLE",
1578
+ message: "runtime_secret_bootstrap requires secretAnchorDeps in OpsRouterDeps",
1579
+ nextStep: "wire_secret_anchor_deps_into_ops_router",
1580
+ },
1581
+ warnings: [],
1582
+ sourceRefs: [],
1583
+ };
1584
+ return envelope;
1585
+ }
1558
1586
  try {
1559
- const { readImpulseContext } = await import("../../core/second-nature/guidance/impulse-context-reader.js");
1560
- const existing = await readImpulseContext(deps.state, sceneType, capabilityIntent, platformId);
1561
- if (existing.available) {
1562
- artifactData = {
1563
- sceneType: existing.artifact.sceneType,
1564
- capabilityIntent: existing.artifact.capabilityIntent,
1565
- platformId: existing.artifact.platformId,
1566
- capabilityClass: existing.artifact.capabilityClass,
1567
- impulseSource: existing.artifact.impulseSource,
1568
- impulseText: existing.artifact.impulseText,
1569
- atmosphereText: existing.artifact.atmosphereText,
1570
- expressionBoundaryConstraints: existing.artifact.expressionBoundaryConstraints,
1571
- expressionBoundaryStyle: existing.artifact.expressionBoundaryStyle,
1572
- freshnessMs: existing.freshnessMs,
1573
- persisted: true,
1574
- };
1575
- sourceRefs.push("core/second-nature/guidance/impulse-context-reader.ts");
1576
- }
1587
+ const view = await viewSecretAnchor(deps.secretAnchorDeps);
1588
+ // Map to RuntimeSecretBootstrapView (design model §6.1)
1589
+ const data = {
1590
+ status: view.status === "verified" || view.status === "ok"
1591
+ ? "ok"
1592
+ : view.status === "missing"
1593
+ ? "runtime_secret_anchor_missing"
1594
+ : view.status === "wrong_key"
1595
+ ? "credential_recovery_required"
1596
+ : view.status === "decryption_failed"
1597
+ ? "runtime_secret_unavailable"
1598
+ : "unknown",
1599
+ keyHealth: view.status === "verified" || view.status === "ok"
1600
+ ? "ok"
1601
+ : view.status === "missing"
1602
+ ? "missing_key"
1603
+ : view.status === "wrong_key"
1604
+ ? "wrong_key"
1605
+ : "unknown",
1606
+ anchorLocation: view.keyPath,
1607
+ recoveryPrincipleRef: view.recoveryDocRef,
1608
+ plaintextKeyExposed: false,
1609
+ reasonCode: view.reasonCode,
1610
+ recoverySteps: view.recoverySteps,
1611
+ };
1612
+ const envelope = {
1613
+ ok: true,
1614
+ command: "runtime_secret_bootstrap",
1615
+ runtimeMode: "workspace_full_runtime",
1616
+ surfaceMode: "cli",
1617
+ generatedAt,
1618
+ data,
1619
+ warnings: [],
1620
+ sourceRefs: ["observability/services/runtime-secret-anchor-view.ts"],
1621
+ };
1622
+ return envelope;
1577
1623
  }
1578
- catch {
1579
- // Reader failure fall through to assembly
1624
+ catch (err) {
1625
+ const msg = err instanceof Error ? err.message : String(err);
1626
+ const envelope = {
1627
+ ok: false,
1628
+ command: "runtime_secret_bootstrap",
1629
+ runtimeMode: "unavailable",
1630
+ surfaceMode: "cli",
1631
+ generatedAt,
1632
+ error: { code: "SECRET_ANCHOR_PROBE_FAILED", message: msg },
1633
+ warnings: [],
1634
+ sourceRefs: [],
1635
+ };
1636
+ return envelope;
1580
1637
  }
1581
1638
  }
1582
- // Real-time assembly if no persisted artifact
1583
- if (!artifactData) {
1584
- const { assembleImpulseSync } = await import("../../guidance/impulse-assembler.js");
1585
- const { buildExpressionBoundary } = await import("../../guidance/output-guard.js");
1586
- const { getShortAtmosphereTemplate } = await import("../../guidance/template-registry.js");
1587
- const impulseResult = assembleImpulseSync({
1588
- sceneType: sceneType,
1589
- capabilityIntent,
1590
- platformId,
1591
- });
1592
- const atmosphere = getShortAtmosphereTemplate("active", "low");
1593
- const expressionBoundary = buildExpressionBoundary(sceneType);
1594
- artifactData = {
1595
- sceneType,
1596
- capabilityIntent: capabilityIntent ?? null,
1597
- platformId: platformId ?? null,
1598
- capabilityClass: impulseResult.capabilityClass,
1599
- impulseSource: impulseResult.source,
1600
- impulseText: impulseResult.impulse?.text ?? null,
1601
- impulseReviewStatus: impulseResult.impulse?.reviewStatus ?? null,
1602
- atmosphereText: atmosphere.text,
1603
- atmosphereReviewStatus: atmosphere.reviewStatus,
1604
- expressionBoundaryConstraints: expressionBoundary.constraints,
1605
- expressionBoundaryStyle: expressionBoundary.style,
1606
- persisted: false,
1607
- };
1608
- if (impulseResult.source === "none") {
1609
- warnings.push("no_impulse_available_for_this_scene_and_capability");
1639
+ // ─── T-V7C.C.4R + T-GVS.R.1: guidance_payload ─────────────────────────
1640
+ // Returns the assembled impulse + atmosphere for a given scene context.
1641
+ // When state DB is wired, reads persisted artifact first; falls back to
1642
+ // real-time assembly and persists for subsequent reads.
1643
+ if (command === "guidance_payload") {
1644
+ const generatedAt = new Date().toISOString();
1645
+ const sceneType = input?.sceneType ?? "social";
1646
+ const capabilityIntent = typeof input?.capabilityIntent === "string"
1647
+ ? input.capabilityIntent
1648
+ : undefined;
1649
+ const platformId = typeof input?.platformId === "string"
1650
+ ? input.platformId
1651
+ : undefined;
1652
+ const validSceneTypes = ["social", "reply", "outreach", "quiet", "heartbeat", "explain", "user_reply"];
1653
+ if (!validSceneTypes.includes(sceneType)) {
1654
+ const envelope = {
1655
+ ok: false,
1656
+ command: "guidance_payload",
1657
+ runtimeMode: "unavailable",
1658
+ surfaceMode: "cli",
1659
+ generatedAt,
1660
+ error: {
1661
+ code: "INVALID_SCENE_TYPE",
1662
+ message: `sceneType must be one of: ${validSceneTypes.join(", ")}`,
1663
+ nextStep: "reinvoke_with_valid_scene_type",
1664
+ },
1665
+ warnings: [],
1666
+ sourceRefs: [],
1667
+ };
1668
+ return envelope;
1610
1669
  }
1611
- // T-GVS.R.1: Persist assembled artifact for future reads
1670
+ // T-GVS.R.1: Try reading persisted artifact first
1671
+ let artifactData;
1672
+ let warnings = [];
1673
+ let sourceRefs = [
1674
+ "guidance/capability-class.ts",
1675
+ "guidance/impulse-assembler.ts",
1676
+ "guidance/template-registry.ts",
1677
+ "guidance/output-guard.ts",
1678
+ ];
1612
1679
  if (deps.state) {
1613
1680
  try {
1614
- const { writeImpulseContext } = await import("../../core/second-nature/guidance/impulse-context-writer.js");
1615
- await writeImpulseContext(deps.state, {
1616
- sceneType,
1617
- capabilityIntent,
1618
- platformId,
1619
- impulseResult,
1620
- atmosphereText: atmosphere.text,
1621
- expressionBoundaryConstraints: expressionBoundary.constraints,
1622
- expressionBoundaryStyle: expressionBoundary.style,
1623
- }, { now: generatedAt });
1624
- sourceRefs.push("core/second-nature/guidance/impulse-context-writer.ts");
1681
+ const { readImpulseContext } = await import("../../core/second-nature/guidance/impulse-context-reader.js");
1682
+ const existing = await readImpulseContext(deps.state, sceneType, capabilityIntent, platformId);
1683
+ if (existing.available) {
1684
+ artifactData = {
1685
+ sceneType: existing.artifact.sceneType,
1686
+ capabilityIntent: existing.artifact.capabilityIntent,
1687
+ platformId: existing.artifact.platformId,
1688
+ capabilityClass: existing.artifact.capabilityClass,
1689
+ impulseSource: existing.artifact.impulseSource,
1690
+ impulseText: existing.artifact.impulseText,
1691
+ atmosphereText: existing.artifact.atmosphereText,
1692
+ expressionBoundaryConstraints: existing.artifact.expressionBoundaryConstraints,
1693
+ expressionBoundaryStyle: existing.artifact.expressionBoundaryStyle,
1694
+ freshnessMs: existing.freshnessMs,
1695
+ persisted: true,
1696
+ };
1697
+ sourceRefs.push("core/second-nature/guidance/impulse-context-reader.ts");
1698
+ }
1625
1699
  }
1626
1700
  catch {
1627
- // Persistence failure is non-fatal; surface still returns assembled payload
1628
- warnings.push("impulse_context_persistence_failed");
1701
+ // Reader failure fall through to assembly
1702
+ }
1703
+ }
1704
+ // Real-time assembly if no persisted artifact
1705
+ if (!artifactData) {
1706
+ const { assembleImpulseSync } = await import("../../guidance/impulse-assembler.js");
1707
+ const { buildExpressionBoundary } = await import("../../guidance/output-guard.js");
1708
+ const { getShortAtmosphereTemplate } = await import("../../guidance/template-registry.js");
1709
+ const impulseResult = assembleImpulseSync({
1710
+ sceneType: sceneType,
1711
+ capabilityIntent,
1712
+ platformId,
1713
+ });
1714
+ const atmosphere = getShortAtmosphereTemplate("active", "low");
1715
+ const expressionBoundary = buildExpressionBoundary(sceneType);
1716
+ artifactData = {
1717
+ sceneType,
1718
+ capabilityIntent: capabilityIntent ?? null,
1719
+ platformId: platformId ?? null,
1720
+ capabilityClass: impulseResult.capabilityClass,
1721
+ impulseSource: impulseResult.source,
1722
+ impulseText: impulseResult.impulse?.text ?? null,
1723
+ impulseReviewStatus: impulseResult.impulse?.reviewStatus ?? null,
1724
+ atmosphereText: atmosphere.text,
1725
+ atmosphereReviewStatus: atmosphere.reviewStatus,
1726
+ expressionBoundaryConstraints: expressionBoundary.constraints,
1727
+ expressionBoundaryStyle: expressionBoundary.style,
1728
+ persisted: false,
1729
+ };
1730
+ if (impulseResult.source === "none") {
1731
+ warnings.push("no_impulse_available_for_this_scene_and_capability");
1732
+ }
1733
+ // T-GVS.R.1: Persist assembled artifact for future reads
1734
+ if (deps.state) {
1735
+ try {
1736
+ const { writeImpulseContext } = await import("../../core/second-nature/guidance/impulse-context-writer.js");
1737
+ await writeImpulseContext(deps.state, {
1738
+ sceneType,
1739
+ capabilityIntent,
1740
+ platformId,
1741
+ impulseResult,
1742
+ atmosphereText: atmosphere.text,
1743
+ expressionBoundaryConstraints: expressionBoundary.constraints,
1744
+ expressionBoundaryStyle: expressionBoundary.style,
1745
+ }, { now: generatedAt });
1746
+ sourceRefs.push("core/second-nature/guidance/impulse-context-writer.ts");
1747
+ }
1748
+ catch {
1749
+ // Persistence failure is non-fatal; surface still returns assembled payload
1750
+ warnings.push("impulse_context_persistence_failed");
1751
+ }
1629
1752
  }
1630
1753
  }
1754
+ const envelope = {
1755
+ ok: true,
1756
+ command: "guidance_payload",
1757
+ runtimeMode: deps.runtimeAvailable ? "workspace_full_runtime" : "host_safe_carrier",
1758
+ surfaceMode: "cli",
1759
+ generatedAt,
1760
+ data: artifactData,
1761
+ warnings,
1762
+ sourceRefs,
1763
+ };
1764
+ return envelope;
1631
1765
  }
1632
- const envelope = {
1633
- ok: true,
1634
- command: "guidance_payload",
1635
- runtimeMode: deps.runtimeAvailable ? "workspace_full_runtime" : "host_safe_carrier",
1636
- surfaceMode: "cli",
1637
- generatedAt,
1638
- data: artifactData,
1639
- warnings,
1640
- sourceRefs,
1766
+ return {
1767
+ ok: false,
1768
+ command,
1769
+ error: {
1770
+ code: "unknown_ops_command",
1771
+ message: `Unknown ops command: ${command}`,
1772
+ },
1641
1773
  };
1642
- return envelope;
1643
- }
1644
- return {
1645
- ok: false,
1646
- error: {
1647
- code: "unknown_ops_command",
1648
- message: `Unknown ops command: ${command}`,
1649
- },
1650
- };
1774
+ })();
1775
+ const envelopeRuntimeMode = typeof input?.runtimeAvailable === "boolean"
1776
+ ? (input.runtimeAvailable ? "workspace_full_runtime" : "host_safe_carrier")
1777
+ : (deps.runtimeAvailable ? "workspace_full_runtime" : "host_safe_carrier");
1778
+ return normalizeEnvelopeResult(rawResult, command, envelopeRuntimeMode);
1651
1779
  },
1652
1780
  };
1653
1781
  }