@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
package/index.js CHANGED
@@ -51,7 +51,7 @@
51
51
  * **same absolute path** as the OpenClaw **agent workspace** (default `~/.openclaw/workspace`, or
52
52
  * `agents.defaults.workspace` in `~/.openclaw/openclaw.json`). Do **not** infer that root from the plugin
53
53
  * install directory. With **sandbox** or **per-agent workspaces**, use the path where `data/state.db` and
54
- * `workspace/` anchors actually live. See `explore/reports/2026-05-04_openclaw-plugin-install-vs-workspace-root.md`.
54
+ * `workspace/` anchors actually live.
55
55
  *
56
56
  * Test coverage:
57
57
  * - tests/integration/cli/plugin-runtime-registration.test.ts
@@ -81,6 +81,20 @@ const HOST_SAFE_LIMITATION_MESSAGE = "Host-safe plugin package keeps synchronous
81
81
  const SETUP_MARKER_RELATIVE_PATH = path.join(".second-nature", "setup", "agent-inner-guide-ack.json");
82
82
  const SETUP_GUIDE_VERSION = "0.1.38";
83
83
  const SETUP_COMMANDS = new Set(["setup_hint", "setup_ack"]);
84
+ // T-SH.R.8: Import shared setup-ack validator instead of duplicating.
85
+ // Source of truth: src/shared/setup-ack.ts (copied to plugin/runtime/shared/ by build).
86
+ import { validateSetupAck as validateSetupAckShared, SETUP_ACK_SCHEMA_VERSION, } from "./runtime/shared/setup-ack.js";
87
+ /**
88
+ * Wrapper that adapts the shared validator's richer return type
89
+ * ({ok:true, ack} | {ok:false, errors}) to the plugin's historical
90
+ * ({ok:true} | {ok:false, errors}) shape. Keeps call sites unchanged.
91
+ */
92
+ function validateSetupAck(raw) {
93
+ const result = validateSetupAckShared(raw);
94
+ if (result.ok)
95
+ return { ok: true };
96
+ return { ok: false, errors: result.errors };
97
+ }
84
98
  let activationSpine = null;
85
99
  /** T1.1.4 — lazily opened full read bridge; closed when workspace root / resolution changes. */
86
100
  let workspaceOpsBridge = null;
@@ -248,6 +262,31 @@ function safeShortText(value, maxLength = 240) {
248
262
  ? `${trimmed.slice(0, maxLength - 3)}...`
249
263
  : trimmed;
250
264
  }
265
+ function buildHostDiscoveryReport(input) {
266
+ const observedAt = new Date().toISOString();
267
+ const hostName = typeof input?.hostName === "string" ? input.hostName : undefined;
268
+ const hostVersion = typeof input?.hostVersion === "string" ? input.hostVersion : undefined;
269
+ return {
270
+ toolDiscovery: {
271
+ status: "unsupported",
272
+ tools: [],
273
+ hostName,
274
+ hostVersion,
275
+ observedAt,
276
+ reason: "host_probe_unsupported",
277
+ },
278
+ skillDiscovery: {
279
+ status: "unsupported",
280
+ skills: [],
281
+ observedAt,
282
+ reason: "skill_probe_unsupported",
283
+ },
284
+ setupComplete: false,
285
+ evidenceLevel: "carrier_ack",
286
+ reason: "host_probe_unsupported",
287
+ nextStep: "host_safe_carrier_cannot_probe_host_registry_run_workspace_cli_for_full_discovery",
288
+ };
289
+ }
251
290
  function resolveSetupMarkerPath(spine) {
252
291
  if (spine.workspaceRootContext.resolution === "unknown") {
253
292
  return undefined;
@@ -264,6 +303,16 @@ function readSetupAckMarker(spine) {
264
303
  }
265
304
  try {
266
305
  const marker = JSON.parse(fs.readFileSync(markerPath, "utf-8"));
306
+ const validation = validateSetupAck(marker);
307
+ if (!validation.ok) {
308
+ return {
309
+ status: "incomplete",
310
+ markerPath,
311
+ acknowledgedAt: marker.acknowledgedAt,
312
+ placedIn: marker.placedIn,
313
+ incompleteReasons: validation.errors,
314
+ };
315
+ }
267
316
  return {
268
317
  status: "acknowledged",
269
318
  markerPath,
@@ -272,7 +321,17 @@ function readSetupAckMarker(spine) {
272
321
  };
273
322
  }
274
323
  catch {
275
- return { status: "pending", markerPath };
324
+ return {
325
+ status: "incomplete",
326
+ markerPath,
327
+ incompleteReasons: [
328
+ {
329
+ field: "marker",
330
+ reason: "Marker file is not valid JSON",
331
+ repairAction: "Re-run setup_ack to rewrite the marker with a valid schema.",
332
+ },
333
+ ],
334
+ };
276
335
  }
277
336
  }
278
337
  function readPackagedSetupText(fileName) {
@@ -327,20 +386,27 @@ function buildSetupHintPayload(spine, input) {
327
386
  const includeSkill = input?.includeSkill !== false;
328
387
  const includeGuide = input?.includeGuide !== false;
329
388
  const ack = readSetupAckMarker(spine);
389
+ const hostDiscovery = buildHostDiscoveryReport(input);
330
390
  const data = {
331
391
  status: ack.status,
332
392
  workspaceRootResolution: spine.workspaceRootContext.resolution,
333
393
  markerPath: ack.markerPath,
334
394
  acknowledgedAt: ack.acknowledgedAt,
335
395
  placedIn: ack.placedIn,
396
+ hostDiscovery,
336
397
  recommendedPlacement: [
337
398
  "agent prompt",
338
399
  "workspace/IDENTITY.md",
339
400
  "workspace/USER.md",
340
401
  ],
341
402
  nextStep: ack.status === "acknowledged"
342
- ? "setup_already_acknowledged"
343
- : "read_returned_guidance_then_run_setup_ack",
403
+ ? hostDiscovery.setupComplete
404
+ ? "setup_verified_by_host_discovery"
405
+ : hostDiscovery.nextStep
406
+ : ack.status === "incomplete"
407
+ ? "repair_setup_ack_fields"
408
+ : "read_returned_guidance_then_run_setup_ack",
409
+ ...(ack.incompleteReasons ? { incompleteReasons: ack.incompleteReasons } : {}),
344
410
  };
345
411
  if (includeSkill) {
346
412
  const skill = readPackagedSetupText("SKILL.md");
@@ -364,6 +430,7 @@ function buildSetupHintPayload(spine, input) {
364
430
  ok: true,
365
431
  command: "setup_hint",
366
432
  surfaceMode: "host_safe_carrier",
433
+ evidenceLevel: "contract_smoke",
367
434
  message: "Read the SKILL and guide as a friendly setup note, then place the guidance where the agent naturally checks its working anchors.",
368
435
  data,
369
436
  };
@@ -382,25 +449,48 @@ function buildSetupAckPayload(spine, input) {
382
449
  },
383
450
  };
384
451
  }
452
+ const placedIn = safeShortText(input?.placedIn, 160);
453
+ const placementProofRef = safeShortText(input?.placementProofRef, 320);
454
+ const writer = safeShortText(input?.writer, 80);
385
455
  const marker = {
456
+ schemaVersion: SETUP_ACK_SCHEMA_VERSION,
386
457
  acknowledgedAt: new Date().toISOString(),
387
458
  acceptedBy: safeShortText(input?.acceptedBy, 80) ?? "agent",
388
- placedIn: safeShortText(input?.placedIn, 160) ?? "unspecified",
459
+ placedIn: placedIn ?? "unspecified",
460
+ placementProofRef: placementProofRef ?? "",
389
461
  note: safeShortText(input?.note, 240),
390
462
  guideVersion: SETUP_GUIDE_VERSION,
391
463
  source: "second-nature-plugin",
392
464
  skillPath: "SKILL.md",
393
465
  guidePath: "agent-inner-guide.md",
466
+ writer: writer ?? "setup_ack_command",
394
467
  };
468
+ const validation = validateSetupAck(marker);
469
+ if (!validation.ok) {
470
+ return {
471
+ ok: false,
472
+ command: "setup_ack",
473
+ surfaceMode: "host_safe_carrier",
474
+ evidenceLevel: "carrier_ack",
475
+ message: "Setup acknowledgement is incomplete; see incompleteReasons and repairAction.",
476
+ data: {
477
+ markerPath,
478
+ incompleteReasons: validation.errors,
479
+ },
480
+ };
481
+ }
395
482
  fs.mkdirSync(path.dirname(markerPath), { recursive: true });
396
483
  fs.writeFileSync(markerPath, `${JSON.stringify(marker, null, 2)}\n`, "utf-8");
484
+ const hostDiscovery = buildHostDiscoveryReport(input);
397
485
  return {
398
486
  ok: true,
399
487
  command: "setup_ack",
400
488
  surfaceMode: "host_safe_carrier",
401
- message: "Setup guide acknowledgement persisted; setup nudge is now silent for this workspace.",
489
+ evidenceLevel: hostDiscovery.evidenceLevel,
490
+ message: "Setup guide acknowledgement persisted; setup nudge is now silent for this workspace. Host skill discovery is unavailable in carrier mode.",
402
491
  data: {
403
492
  markerPath,
493
+ hostDiscovery,
404
494
  ...marker,
405
495
  },
406
496
  };
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "second-nature",
3
3
  "name": "Second Nature",
4
- "version": "0.2.12",
4
+ "version": "0.2.13",
5
5
  "description": "OpenClaw native plugin with synchronous surface registration and bundled runtime spine. Set SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot to the same path as the agent workspace. Agent inner guide is packaged as agent-inner-guide.md. Ops surface: loop_status, self_health, tool_affordance, heartbeat_check, heartbeat_run, heartbeat_digest, snapshot:capture, narrative:diff, timeline, restore, runtime_secret_bootstrap, connector:run, guidance_payload.",
6
6
  "activation": {
7
7
  "onStartup": true
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haaaiawd/second-nature",
3
- "version": "0.2.12",
3
+ "version": "0.2.13",
4
4
  "description": "OpenClaw native plugin with synchronous registration, a packaged runtime artifact, and operator-facing status/explain flows.",
5
5
  "keywords": [
6
6
  "openclaw",
@@ -7,6 +7,8 @@ import { explainSurfaceSubject } from "../explain/explain-surface-subject.js";
7
7
  import { showOperatorFallback, OperatorFallbackNotFoundError, } from "../ops/show-operator-fallback.js";
8
8
  import { runStorageModeSmoke } from "../../storage/bootstrap/storage-mode-smoke.js";
9
9
  import { policySet } from "./policy.js";
10
+ import { validateSetupAck, SETUP_ACK_SCHEMA_VERSION } from "../../shared/setup-ack.js";
11
+ import { createDefaultHostDiscoveryPort, probeHostDiscovery, recordHostToolVisibilityLog, } from "../host-capability/host-discovery-port.js";
10
12
  const SETUP_MARKER_RELATIVE_PATH = path.join(".second-nature", "setup", "agent-inner-guide-ack.json");
11
13
  function safeShortText(value, maxLen) {
12
14
  if (typeof value !== "string")
@@ -53,11 +55,21 @@ function readSetupAckMarker(workspaceRoot) {
53
55
  const raw = fs.readFileSync(markerPath, "utf-8");
54
56
  const marker = JSON.parse(raw);
55
57
  if (marker.status === "acknowledged") {
58
+ const validation = validateSetupAck(marker);
59
+ if (validation.ok) {
60
+ return {
61
+ status: "acknowledged",
62
+ markerPath,
63
+ acknowledgedAt: validation.ack.acknowledgedAt,
64
+ placedIn: validation.ack.placedIn,
65
+ };
66
+ }
56
67
  return {
57
- status: "acknowledged",
68
+ status: "incomplete",
58
69
  markerPath,
59
70
  acknowledgedAt: typeof marker.acknowledgedAt === "string" ? marker.acknowledgedAt : undefined,
60
71
  placedIn: typeof marker.placedIn === "string" ? marker.placedIn : undefined,
72
+ incompleteReasons: validation.errors,
61
73
  };
62
74
  }
63
75
  }
@@ -72,20 +84,32 @@ async function buildSetupHintPayload(input) {
72
84
  const includeGuide = input?.includeGuide !== false;
73
85
  const workspaceRoot = resolveWorkspaceRoot(input);
74
86
  const ack = readSetupAckMarker(workspaceRoot);
87
+ const hostDiscovery = await probeHostDiscovery({
88
+ port: createDefaultHostDiscoveryPort(),
89
+ hostName: typeof input?.hostName === "string" ? input.hostName : undefined,
90
+ hostVersion: typeof input?.hostVersion === "string" ? input.hostVersion : undefined,
91
+ });
92
+ await recordHostToolVisibilityLog(workspaceRoot, "setup_hint", hostDiscovery);
75
93
  const data = {
76
94
  status: ack.status,
77
95
  workspaceRoot,
78
96
  markerPath: ack.markerPath,
79
97
  acknowledgedAt: ack.acknowledgedAt,
80
98
  placedIn: ack.placedIn,
99
+ hostDiscovery,
81
100
  recommendedPlacement: [
82
101
  "agent prompt",
83
102
  "workspace/IDENTITY.md",
84
103
  "workspace/USER.md",
85
104
  ],
86
105
  nextStep: ack.status === "acknowledged"
87
- ? "setup_already_acknowledged"
88
- : "read_returned_guidance_then_run_setup_ack",
106
+ ? hostDiscovery.setupComplete
107
+ ? "setup_verified_by_host_discovery"
108
+ : hostDiscovery.nextStep
109
+ : ack.status === "incomplete"
110
+ ? "repair_setup_ack_fields"
111
+ : "read_returned_guidance_then_run_setup_ack",
112
+ ...(ack.incompleteReasons ? { incompleteReasons: ack.incompleteReasons } : {}),
89
113
  };
90
114
  if (includeSkill) {
91
115
  const skill = readSetupText("SKILL.md");
@@ -108,35 +132,76 @@ async function buildSetupHintPayload(input) {
108
132
  return {
109
133
  ok: true,
110
134
  command: "setup_hint",
135
+ runtimeMode: "workspace_full_runtime",
111
136
  surfaceMode: "workspace_full_runtime",
112
- message: "Read the SKILL and guide as a friendly setup note, then place the guidance where the agent naturally checks its working anchors.",
113
- data,
137
+ generatedAt: new Date().toISOString(),
138
+ evidenceLevel: "contract_smoke",
139
+ warnings: [],
140
+ sourceRefs: [],
141
+ data: {
142
+ message: "Read the SKILL and guide as a friendly setup note, then place the guidance where the agent naturally checks its working anchors.",
143
+ ...data,
144
+ },
114
145
  };
115
146
  }
116
147
  async function buildSetupAckPayload(input) {
117
148
  const workspaceRoot = resolveWorkspaceRoot(input);
118
149
  const markerPath = path.join(workspaceRoot, SETUP_MARKER_RELATIVE_PATH);
119
- const marker = {
150
+ const placedIn = safeShortText(input?.placedIn, 160);
151
+ const placementProofRef = safeShortText(input?.placementProofRef, 320);
152
+ const writer = safeShortText(input?.writer, 80);
153
+ const candidate = {
154
+ schemaVersion: SETUP_ACK_SCHEMA_VERSION,
120
155
  acknowledgedAt: new Date().toISOString(),
121
156
  acceptedBy: safeShortText(input?.acceptedBy, 80) ?? "agent",
122
- placedIn: safeShortText(input?.placedIn, 160) ?? "unspecified",
157
+ placedIn: placedIn ?? "unspecified",
158
+ placementProofRef: placementProofRef ?? "",
123
159
  note: safeShortText(input?.note, 240),
124
160
  guideVersion: "0.2.5",
161
+ writer: writer ?? "setup_ack_command",
125
162
  source: "second-nature-cli",
126
163
  skillPath: "SKILL.md",
127
164
  guidePath: "plugin/agent-inner-guide.md",
128
165
  status: "acknowledged",
129
166
  };
167
+ const validation = validateSetupAck(candidate);
168
+ if (!validation.ok) {
169
+ return {
170
+ ok: false,
171
+ command: "setup_ack",
172
+ surfaceMode: "workspace_full_runtime",
173
+ evidenceLevel: "carrier_ack",
174
+ message: "Setup acknowledgement is incomplete; see errors and repairAction.",
175
+ data: {
176
+ markerPath,
177
+ validationErrors: validation.errors,
178
+ },
179
+ };
180
+ }
130
181
  fs.mkdirSync(path.dirname(markerPath), { recursive: true });
131
- fs.writeFileSync(markerPath, `${JSON.stringify(marker, null, 2)}\n`, "utf-8");
182
+ fs.writeFileSync(markerPath, `${JSON.stringify(candidate, null, 2)}\n`, "utf-8");
183
+ const hostDiscovery = await probeHostDiscovery({
184
+ port: createDefaultHostDiscoveryPort(),
185
+ hostName: typeof input?.hostName === "string" ? input.hostName : undefined,
186
+ hostVersion: typeof input?.hostVersion === "string" ? input.hostVersion : undefined,
187
+ });
188
+ await recordHostToolVisibilityLog(workspaceRoot, "setup_ack", hostDiscovery);
132
189
  return {
133
190
  ok: true,
134
191
  command: "setup_ack",
192
+ runtimeMode: "workspace_full_runtime",
135
193
  surfaceMode: "workspace_full_runtime",
136
- message: "Setup guide acknowledgement persisted; setup nudge is now silent for this workspace.",
194
+ generatedAt: new Date().toISOString(),
195
+ evidenceLevel: hostDiscovery.setupComplete ? "state_present" : "carrier_ack",
196
+ warnings: hostDiscovery.setupComplete ? [] : ["host_discovery_incomplete"],
197
+ sourceRefs: [],
137
198
  data: {
199
+ message: hostDiscovery.setupComplete
200
+ ? "Setup guide acknowledgement persisted and host discovery confirms tool/skill visibility."
201
+ : "Setup guide acknowledgement persisted, but host discovery has not confirmed tool/skill visibility; see hostDiscovery.",
138
202
  markerPath,
139
- ...marker,
203
+ hostDiscovery,
204
+ ...candidate,
140
205
  },
141
206
  };
142
207
  }
@@ -322,7 +387,16 @@ export function createCliCommands(deps) {
322
387
  },
323
388
  {
324
389
  name: "heartbeat_check",
325
- description: "Workspace heartbeat_check ops surface (v5 HeartbeatSurfaceResult)",
390
+ description: "v8 living-loop heartbeat runs real-runtime perception/judgment/action closure",
391
+ execute: async (input) => {
392
+ const surface = await Promise.resolve(opsRouter.dispatch("heartbeat_check", input));
393
+ flush();
394
+ return surface;
395
+ },
396
+ },
397
+ {
398
+ name: "heartbeat",
399
+ description: "[DEPRECATED] v7 heartbeat entrypoint; alias to heartbeat_check",
326
400
  execute: async (input) => {
327
401
  const surface = await Promise.resolve(opsRouter.dispatch("heartbeat_check", input));
328
402
  flush();
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Host Capability Discovery Port (T-ROS.R.7, T-ROS.R.9)
3
+ *
4
+ * Core logic: provide an explicit boundary for proving that the Second Nature
5
+ * tool (`second_nature_ops`) and packaged skill are visible to the host.
6
+ *
7
+ * Design authority:
8
+ * - `.anws/v8/04_SYSTEM_DESIGN/runtime-ops-system.md §3.1`
9
+ *
10
+ * Dependencies: none (plain contracts)
11
+ * Boundary: pure interface + a default fail-closed adapter that reports
12
+ * `host_probe_unsupported` rather than inventing a discovery proof.
13
+ *
14
+ * T-ROS.R.9 manual-smoke-only contract (option b):
15
+ * In carrier/packaged mode, there is no OpenClaw host API available for
16
+ * introspection. The default adapter returns `unsupported` with
17
+ * `host_probe_unsupported` reason. The positive path (T-ROS.R.5
18
+ * "host-visible second_nature_ops") is borne by external manual host smoke.
19
+ * `05B_VERIFICATION_PLAN.md` marks T-ROS.R.5 as "requires manual host evidence"
20
+ * with required fields: hostName, hostVersion, timestamp, raw tool list JSON.
21
+ * Callers must not promote `evidenceLevel` beyond `carrier_ack` without
22
+ * a real `HostCapabilityDiscoveryPort` implementation or manual smoke proof.
23
+ *
24
+ * Test coverage: tests/unit/cli/host-discovery-port.test.ts
25
+ */
26
+ export type HostDiscoveryStatus = "available" | "unavailable" | "unsupported" | "blocked";
27
+ export type HostToolUnavailableReason = "host_tool_unavailable" | "host_probe_unsupported" | "host_policy_blocked" | "host_probe_timeout";
28
+ export type HostSkillUnavailableReason = "skill_projection_unavailable" | "skill_probe_unsupported" | "host_policy_blocked" | "host_probe_timeout";
29
+ export interface HostToolDiscoveryResult {
30
+ status: HostDiscoveryStatus;
31
+ tools: string[];
32
+ hostName?: string;
33
+ hostVersion?: string;
34
+ observedAt: string;
35
+ reason?: HostToolUnavailableReason;
36
+ }
37
+ export interface HostSkillDiscoveryResult {
38
+ status: HostDiscoveryStatus;
39
+ skills: string[];
40
+ observedAt: string;
41
+ reason?: HostSkillUnavailableReason;
42
+ }
43
+ export interface HostCapabilityDiscoveryPort {
44
+ /** Prove that `second_nature_ops` is visible in the current host session. */
45
+ listHostTools(): Promise<HostToolDiscoveryResult>;
46
+ /** Prove that the packaged skill is discoverable by the host skill registry. */
47
+ listHostSkills?(): Promise<HostSkillDiscoveryResult>;
48
+ }
49
+ export interface HostDiscoveryProbeOptions {
50
+ port: HostCapabilityDiscoveryPort;
51
+ hostName?: string;
52
+ hostVersion?: string;
53
+ }
54
+ export interface HostDiscoveryReport {
55
+ toolDiscovery: HostToolDiscoveryResult;
56
+ skillDiscovery: HostSkillDiscoveryResult;
57
+ setupComplete: boolean;
58
+ /** Evidence level for setup state after applying discovery truth. */
59
+ evidenceLevel: "carrier_ack" | "contract_smoke" | "state_present";
60
+ reason?: HostToolUnavailableReason | HostSkillUnavailableReason;
61
+ nextStep: string;
62
+ }
63
+ /**
64
+ * Default fail-closed adapter. It does not invent host API access;
65
+ * it returns explicit `unsupported` diagnostics so callers cannot
66
+ * promote setup to `real_runtime` without real host evidence.
67
+ */
68
+ export declare function createDefaultHostDiscoveryPort(): HostCapabilityDiscoveryPort;
69
+ export declare function probeHostDiscovery(options: HostDiscoveryProbeOptions): Promise<HostDiscoveryReport>;
70
+ export interface HostToolVisibilityLogEntry {
71
+ observedAt: string;
72
+ hostName?: string;
73
+ hostVersion?: string;
74
+ command: string;
75
+ toolDiscovery: HostToolDiscoveryResult;
76
+ skillDiscovery: HostSkillDiscoveryResult;
77
+ evidenceLevel: HostDiscoveryReport["evidenceLevel"];
78
+ setupComplete: boolean;
79
+ }
80
+ /**
81
+ * Persist a host tool/skill visibility log under the workspace so manual host
82
+ * smoke appendices can include timestamp, hostName, hostVersion, raw tool list,
83
+ * command envelope, and evidenceLevel.
84
+ */
85
+ export declare function recordHostToolVisibilityLog(workspaceRoot: string, command: string, report: HostDiscoveryReport): Promise<void>;
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Host Capability Discovery Port (T-ROS.R.7, T-ROS.R.9)
3
+ *
4
+ * Core logic: provide an explicit boundary for proving that the Second Nature
5
+ * tool (`second_nature_ops`) and packaged skill are visible to the host.
6
+ *
7
+ * Design authority:
8
+ * - `.anws/v8/04_SYSTEM_DESIGN/runtime-ops-system.md §3.1`
9
+ *
10
+ * Dependencies: none (plain contracts)
11
+ * Boundary: pure interface + a default fail-closed adapter that reports
12
+ * `host_probe_unsupported` rather than inventing a discovery proof.
13
+ *
14
+ * T-ROS.R.9 manual-smoke-only contract (option b):
15
+ * In carrier/packaged mode, there is no OpenClaw host API available for
16
+ * introspection. The default adapter returns `unsupported` with
17
+ * `host_probe_unsupported` reason. The positive path (T-ROS.R.5
18
+ * "host-visible second_nature_ops") is borne by external manual host smoke.
19
+ * `05B_VERIFICATION_PLAN.md` marks T-ROS.R.5 as "requires manual host evidence"
20
+ * with required fields: hostName, hostVersion, timestamp, raw tool list JSON.
21
+ * Callers must not promote `evidenceLevel` beyond `carrier_ack` without
22
+ * a real `HostCapabilityDiscoveryPort` implementation or manual smoke proof.
23
+ *
24
+ * Test coverage: tests/unit/cli/host-discovery-port.test.ts
25
+ */
26
+ import fs from "node:fs";
27
+ import path from "node:path";
28
+ const SECOND_NATURE_SKILL_ID = "second-nature";
29
+ const SECOND_NATURE_OPS_TOOL = "second_nature_ops";
30
+ /**
31
+ * Default fail-closed adapter. It does not invent host API access;
32
+ * it returns explicit `unsupported` diagnostics so callers cannot
33
+ * promote setup to `real_runtime` without real host evidence.
34
+ */
35
+ export function createDefaultHostDiscoveryPort() {
36
+ return {
37
+ async listHostTools() {
38
+ return {
39
+ status: "unsupported",
40
+ tools: [],
41
+ observedAt: new Date().toISOString(),
42
+ reason: "host_probe_unsupported",
43
+ };
44
+ },
45
+ async listHostSkills() {
46
+ return {
47
+ status: "unsupported",
48
+ skills: [],
49
+ observedAt: new Date().toISOString(),
50
+ reason: "skill_probe_unsupported",
51
+ };
52
+ },
53
+ };
54
+ }
55
+ function capSetupEvidenceLevel(toolStatus, skillStatus) {
56
+ if (toolStatus !== "available" || skillStatus !== "available") {
57
+ return "carrier_ack";
58
+ }
59
+ return "state_present";
60
+ }
61
+ export async function probeHostDiscovery(options) {
62
+ const { port, hostName, hostVersion } = options;
63
+ const toolDiscovery = await port.listHostTools();
64
+ const skillDiscovery = port.listHostSkills
65
+ ? await port.listHostSkills()
66
+ : {
67
+ status: "unsupported",
68
+ skills: [],
69
+ observedAt: new Date().toISOString(),
70
+ reason: "skill_probe_unsupported",
71
+ };
72
+ const toolOk = toolDiscovery.status === "available" &&
73
+ toolDiscovery.tools.includes(SECOND_NATURE_OPS_TOOL);
74
+ const skillOk = skillDiscovery.status === "available" &&
75
+ skillDiscovery.skills.includes(SECOND_NATURE_SKILL_ID);
76
+ const setupComplete = toolOk && skillOk;
77
+ const evidenceLevel = capSetupEvidenceLevel(toolDiscovery.status, skillDiscovery.status);
78
+ let reason;
79
+ let nextStep;
80
+ if (!toolOk) {
81
+ reason = toolDiscovery.reason ?? "host_tool_unavailable";
82
+ nextStep =
83
+ "confirm_second_nature_ops_is_enabled_in_host_tool_registry_and_re_run_setup_hint";
84
+ }
85
+ else if (!skillOk) {
86
+ reason = skillDiscovery.reason ?? "skill_projection_unavailable";
87
+ nextStep =
88
+ "confirm_packaged_SKILL.md_is_indexed_by_host_skill_registry_and_re_run_setup_hint";
89
+ }
90
+ else {
91
+ nextStep = "setup_verified_by_host_discovery";
92
+ }
93
+ return {
94
+ toolDiscovery: {
95
+ ...toolDiscovery,
96
+ hostName,
97
+ hostVersion,
98
+ },
99
+ skillDiscovery,
100
+ setupComplete,
101
+ evidenceLevel,
102
+ reason,
103
+ nextStep,
104
+ };
105
+ }
106
+ /**
107
+ * Persist a host tool/skill visibility log under the workspace so manual host
108
+ * smoke appendices can include timestamp, hostName, hostVersion, raw tool list,
109
+ * command envelope, and evidenceLevel.
110
+ */
111
+ export async function recordHostToolVisibilityLog(workspaceRoot, command, report) {
112
+ const logDir = path.join(workspaceRoot, ".second-nature", "logs");
113
+ fs.mkdirSync(logDir, { recursive: true });
114
+ const logPath = path.join(logDir, "host-tool-visibility.json");
115
+ const entry = {
116
+ observedAt: new Date().toISOString(),
117
+ hostName: report.toolDiscovery.hostName,
118
+ hostVersion: report.toolDiscovery.hostVersion,
119
+ command,
120
+ toolDiscovery: report.toolDiscovery,
121
+ skillDiscovery: report.skillDiscovery,
122
+ evidenceLevel: report.evidenceLevel,
123
+ setupComplete: report.setupComplete,
124
+ };
125
+ let existing = [];
126
+ try {
127
+ const raw = fs.readFileSync(logPath, "utf-8");
128
+ const parsed = JSON.parse(raw);
129
+ if (Array.isArray(parsed))
130
+ existing = parsed;
131
+ }
132
+ catch {
133
+ // File missing or unreadable — start fresh.
134
+ }
135
+ existing.push(entry);
136
+ fs.writeFileSync(logPath, `${JSON.stringify(existing.slice(-50), null, 2)}\n`, "utf-8");
137
+ }
@@ -35,7 +35,7 @@ export interface HeartbeatSurfaceResult {
35
35
  /** True when structured fields mirror a fake adapter for schema parity only */
36
36
  schemaParityOnly?: boolean;
37
37
  /** T-CP.R.2: v8 real runtime spine result when state-backed action-closure spine ran */
38
- v8Spine?: RealRuntimeSpineResult & {
38
+ v8Spine?: Partial<RealRuntimeSpineResult> & {
39
39
  degradedReason?: string;
40
40
  };
41
41
  /** T-GVS.R.1: agent-facing impulse context artifact read pointer */
@@ -101,8 +101,8 @@ export interface HeartbeatCheckInput {
101
101
  /** T-OBS.R.1: shared audit sink for connector/Quiet events consumed by heartbeat_digest. */
102
102
  auditStore?: AppendOnlyAuditStore;
103
103
  /**
104
- * T-CP.R.2: when true and state DB is wired, runs the v8 real runtime action-closure spine
105
- * in addition to the v7 heartbeat loop. Produces state-backed closure/no-action records.
104
+ * T-CP.R.5: v8 living-loop spine is the default operator-facing heartbeat model.
105
+ * Explicit false can be used by legacy callers to force a carrier-only path.
106
106
  */
107
107
  v8SpineEnabled?: boolean;
108
108
  }
@@ -106,8 +106,9 @@ export async function heartbeatCheck(input) {
106
106
  try {
107
107
  const cycle = await run(signal);
108
108
  const surfaceResult = mapCycleToSurface(cycle, "workspace_full_runtime");
109
- // T-CP.R.2: run v8 real runtime spine when enabled and state is available
110
- if (input.v8SpineEnabled && input.state && input.workspaceRoot) {
109
+ // T-CP.R.5: v8 living-loop spine is the default operator-facing model when state is wired
110
+ const v8SpineEnabled = input.v8SpineEnabled !== false && Boolean(input.state && input.workspaceRoot);
111
+ if (v8SpineEnabled && input.state && input.workspaceRoot) {
111
112
  try {
112
113
  const v8Result = await runRealRuntimeHeartbeatCycle({
113
114
  workspaceRoot: input.workspaceRoot,
@@ -115,10 +116,10 @@ export async function heartbeatCheck(input) {
115
116
  requestedAt: timestamp,
116
117
  trigger: "host",
117
118
  });
118
- if ("status" in v8Result && v8Result.status === "degraded") {
119
+ if ("status" in v8Result && "operatorNextAction" in v8Result) {
120
+ // T-CP.R.6: degraded path must not fabricate cycleId/cycleSequence.
121
+ // Only set degradedReason; cycleId/cycleSequence remain absent.
119
122
  surfaceResult.v8Spine = {
120
- cycleId: "",
121
- cycleSequence: 0,
122
123
  degradedReason: v8Result.reason,
123
124
  };
124
125
  surfaceResult.reasons = [