@haaaiawd/second-nature 0.2.9 → 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 (115) hide show
  1. package/index.js +102 -6
  2. package/openclaw.plugin.json +2 -5
  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/host-capability/probe-host-capability.js +1 -1
  8. package/runtime/cli/host-capability/types.d.ts +2 -7
  9. package/runtime/cli/host-capability/types.js +0 -5
  10. package/runtime/cli/ops/heartbeat-surface.d.ts +5 -3
  11. package/runtime/cli/ops/heartbeat-surface.js +38 -8
  12. package/runtime/cli/ops/ops-router.d.ts +6 -2
  13. package/runtime/cli/ops/ops-router.js +1275 -1147
  14. package/runtime/cli/ops/workspace-heartbeat-runner.js +2 -5
  15. package/runtime/connectors/base/normalized-evidence-content.d.ts +4 -0
  16. package/runtime/connectors/base/normalized-evidence-content.js +21 -2
  17. package/runtime/connectors/evidence-normalizer.js +32 -1
  18. package/runtime/connectors/manifest/manifest-schema.d.ts +2 -2
  19. package/runtime/connectors/registry/dynamic-connector-registry.js +16 -1
  20. package/runtime/connectors/services/connector-executor-adapter.js +54 -35
  21. package/runtime/core/second-nature/action/action-closure-recorder.d.ts +4 -0
  22. package/runtime/core/second-nature/action/action-closure-recorder.js +51 -38
  23. package/runtime/core/second-nature/action/action-proposal-builder.js +8 -34
  24. package/runtime/core/second-nature/action/policy-bound-dispatch.d.ts +2 -0
  25. package/runtime/core/second-nature/action/policy-bound-dispatch.js +10 -5
  26. package/runtime/core/second-nature/control-plane/accepted-projection-loader.js +1 -11
  27. package/runtime/core/second-nature/control-plane/cycle-finalizer.d.ts +82 -0
  28. package/runtime/core/second-nature/control-plane/cycle-finalizer.js +187 -0
  29. package/runtime/core/second-nature/control-plane/heartbeat-orchestrator.d.ts +1 -0
  30. package/runtime/core/second-nature/control-plane/heartbeat-orchestrator.js +23 -15
  31. package/runtime/core/second-nature/control-plane/real-runtime-spine.d.ts +2 -0
  32. package/runtime/core/second-nature/control-plane/real-runtime-spine.js +2 -1
  33. package/runtime/core/second-nature/guidance/guidance-proposal-consumer.d.ts +2 -1
  34. package/runtime/core/second-nature/guidance/guidance-proposal-consumer.js +4 -2
  35. package/runtime/core/second-nature/heartbeat/heartbeat-loop.js +4 -3
  36. package/runtime/core/second-nature/heartbeat/runtime-snapshot.d.ts +3 -2
  37. package/runtime/core/second-nature/heartbeat/snapshot-builder.d.ts +3 -2
  38. package/runtime/core/second-nature/orchestrator/intent-planner.js +4 -2
  39. package/runtime/core/second-nature/orchestrator/narrative-update.js +1 -2
  40. package/runtime/core/second-nature/orchestrator/platform-capability-router.d.ts +2 -2
  41. package/runtime/core/second-nature/orchestrator/platform-capability-router.js +1 -1
  42. package/runtime/core/second-nature/outreach/build-outreach-draft-request.js +2 -3
  43. package/runtime/core/second-nature/outreach/dispatch-user-outreach.d.ts +2 -2
  44. package/runtime/core/second-nature/outreach/dispatch-user-outreach.js +6 -1
  45. package/runtime/core/second-nature/outreach/judge-input-from-snapshot.js +3 -14
  46. package/runtime/core/second-nature/outreach/judge-outreach.d.ts +6 -5
  47. package/runtime/core/second-nature/perception/judgment-engine.js +10 -16
  48. package/runtime/core/second-nature/perception/perception-builder.js +15 -11
  49. package/runtime/core/second-nature/quiet/run-source-backed-quiet.js +13 -15
  50. package/runtime/core/second-nature/quiet-dream/daily-rhythm-scheduler.js +40 -16
  51. package/runtime/core/second-nature/quiet-dream/dream-consolidation-runner.d.ts +5 -1
  52. package/runtime/core/second-nature/quiet-dream/dream-consolidation-runner.js +68 -29
  53. package/runtime/core/second-nature/quiet-dream/dream-scheduler.js +2 -3
  54. package/runtime/core/second-nature/quiet-dream/memory-projection-lifecycle.js +2 -13
  55. package/runtime/core/second-nature/quiet-dream/quiet-daily-review-builder.d.ts +1 -0
  56. package/runtime/core/second-nature/quiet-dream/quiet-daily-review-builder.js +34 -11
  57. package/runtime/core/second-nature/types.d.ts +2 -9
  58. package/runtime/dream/dream-engine.js +11 -4
  59. package/runtime/guidance/outreach-draft-schema.d.ts +12 -12
  60. package/runtime/guidance/persona-selection.js +5 -0
  61. package/runtime/guidance/template-registry.js +6 -1
  62. package/runtime/guidance/types.d.ts +2 -2
  63. package/runtime/observability/causal-loop-health.d.ts +2 -1
  64. package/runtime/observability/causal-loop-health.js +7 -0
  65. package/runtime/observability/living-loop-health-gate.js +2 -8
  66. package/runtime/observability/loop-stage-event-sink.js +6 -2
  67. package/runtime/observability/loop-status.d.ts +2 -0
  68. package/runtime/observability/loop-status.js +14 -1
  69. package/runtime/observability/services/heartbeat-digest-assembler.d.ts +3 -0
  70. package/runtime/observability/services/heartbeat-digest-assembler.js +9 -0
  71. package/runtime/observability/services/lived-experience-audit.d.ts +7 -7
  72. package/runtime/shared/degraded-status-classifier.d.ts +16 -0
  73. package/runtime/shared/degraded-status-classifier.js +68 -0
  74. package/runtime/shared/evidence-level-classifier.d.ts +61 -0
  75. package/runtime/shared/evidence-level-classifier.js +116 -0
  76. package/runtime/shared/provenance-tier.d.ts +37 -0
  77. package/runtime/shared/provenance-tier.js +97 -0
  78. package/runtime/shared/serialization.d.ts +17 -0
  79. package/runtime/shared/serialization.js +27 -0
  80. package/runtime/shared/setup-ack.d.ts +54 -0
  81. package/runtime/shared/setup-ack.js +108 -0
  82. package/runtime/shared/source-ref-compat.d.ts +26 -0
  83. package/runtime/shared/source-ref-compat.js +64 -0
  84. package/runtime/shared/types/goal.d.ts +4 -4
  85. package/runtime/shared/types/goal.js +1 -1
  86. package/runtime/shared/types/index.d.ts +1 -0
  87. package/runtime/shared/types/index.js +1 -3
  88. package/runtime/shared/types/source-ref.d.ts +2 -2
  89. package/runtime/shared/types/v7-entities.d.ts +5 -5
  90. package/runtime/shared/types/v7-entities.js +1 -1
  91. package/runtime/shared/types/v8-contracts.d.ts +13 -2
  92. package/runtime/storage/db/index.js +60 -12
  93. package/runtime/storage/db/migrations/index.js +4 -0
  94. package/runtime/storage/db/migrations/v8-004-schema-closure.d.ts +19 -0
  95. package/runtime/storage/db/migrations/v8-004-schema-closure.js +74 -0
  96. package/runtime/storage/db/migrations/v8-005-single-status-schema.d.ts +11 -0
  97. package/runtime/storage/db/migrations/v8-005-single-status-schema.js +16 -0
  98. package/runtime/storage/db/migrations/v8-006-loop-stage-event-proof-trace-columns.d.ts +9 -0
  99. package/runtime/storage/db/migrations/v8-006-loop-stage-event-proof-trace-columns.js +15 -0
  100. package/runtime/storage/db/schema/v8-entities.d.ts +65 -84
  101. package/runtime/storage/db/schema/v8-entities.js +8 -7
  102. package/runtime/storage/delivery/types.d.ts +2 -2
  103. package/runtime/storage/fallback/load-operator-fallback.d.ts +2 -2
  104. package/runtime/storage/fallback/operator-fallback-types.d.ts +2 -2
  105. package/runtime/storage/fallback/operator-fallback-view.d.ts +2 -2
  106. package/runtime/storage/index.d.ts +1 -1
  107. package/runtime/storage/life-evidence/types.d.ts +5 -5
  108. package/runtime/storage/quiet/quiet-artifact-types.d.ts +4 -4
  109. package/runtime/storage/quiet/quiet-artifact-writer.d.ts +2 -2
  110. package/runtime/storage/services/write-validation-gate.d.ts +1 -1
  111. package/runtime/storage/services/write-validation-gate.js +15 -3
  112. package/runtime/storage/snapshots/types.d.ts +8 -8
  113. package/runtime/storage/user-interest/types.d.ts +3 -3
  114. package/runtime/storage/v8-state-stores.d.ts +15 -3
  115. package/runtime/storage/v8-state-stores.js +60 -39
package/index.js CHANGED
@@ -31,6 +31,12 @@
31
31
  * illusion of a working plugin while agent sessions see no tool. We hit
32
32
  * exactly that on 2026-05-06; the fix lives in plugin/openclaw.plugin.json
33
33
  * under the `activation` block.
34
+ * - Cloud Feishu/OpenClaw sessions can report `capabilities=none`. In that
35
+ * mode, binding activation to `onCapabilities:["tool"]` prevents the agent
36
+ * tool from entering the session even though the plugin loaded. Therefore
37
+ * the manifest keeps `activation.onStartup === true` for daemon loading and
38
+ * declares the tool through `contracts.tools`, but does not require a session
39
+ * capability to activate `second_nature_ops`.
34
40
  * - `Shape: non-capability` reported by `openclaw plugins info` is EXPECTED
35
41
  * for this plugin. OpenClaw counts capabilities only across cli-backend /
36
42
  * text-inference / speech / realtime-* / media-understanding /
@@ -45,7 +51,7 @@
45
51
  * **same absolute path** as the OpenClaw **agent workspace** (default `~/.openclaw/workspace`, or
46
52
  * `agents.defaults.workspace` in `~/.openclaw/openclaw.json`). Do **not** infer that root from the plugin
47
53
  * install directory. With **sandbox** or **per-agent workspaces**, use the path where `data/state.db` and
48
- * `workspace/` anchors actually live. See `explore/reports/2026-05-04_openclaw-plugin-install-vs-workspace-root.md`.
54
+ * `workspace/` anchors actually live.
49
55
  *
50
56
  * Test coverage:
51
57
  * - tests/integration/cli/plugin-runtime-registration.test.ts
@@ -75,6 +81,20 @@ const HOST_SAFE_LIMITATION_MESSAGE = "Host-safe plugin package keeps synchronous
75
81
  const SETUP_MARKER_RELATIVE_PATH = path.join(".second-nature", "setup", "agent-inner-guide-ack.json");
76
82
  const SETUP_GUIDE_VERSION = "0.1.38";
77
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
+ }
78
98
  let activationSpine = null;
79
99
  /** T1.1.4 — lazily opened full read bridge; closed when workspace root / resolution changes. */
80
100
  let workspaceOpsBridge = null;
@@ -242,6 +262,31 @@ function safeShortText(value, maxLength = 240) {
242
262
  ? `${trimmed.slice(0, maxLength - 3)}...`
243
263
  : trimmed;
244
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
+ }
245
290
  function resolveSetupMarkerPath(spine) {
246
291
  if (spine.workspaceRootContext.resolution === "unknown") {
247
292
  return undefined;
@@ -258,6 +303,16 @@ function readSetupAckMarker(spine) {
258
303
  }
259
304
  try {
260
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
+ }
261
316
  return {
262
317
  status: "acknowledged",
263
318
  markerPath,
@@ -266,7 +321,17 @@ function readSetupAckMarker(spine) {
266
321
  };
267
322
  }
268
323
  catch {
269
- 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
+ };
270
335
  }
271
336
  }
272
337
  function readPackagedSetupText(fileName) {
@@ -321,20 +386,27 @@ function buildSetupHintPayload(spine, input) {
321
386
  const includeSkill = input?.includeSkill !== false;
322
387
  const includeGuide = input?.includeGuide !== false;
323
388
  const ack = readSetupAckMarker(spine);
389
+ const hostDiscovery = buildHostDiscoveryReport(input);
324
390
  const data = {
325
391
  status: ack.status,
326
392
  workspaceRootResolution: spine.workspaceRootContext.resolution,
327
393
  markerPath: ack.markerPath,
328
394
  acknowledgedAt: ack.acknowledgedAt,
329
395
  placedIn: ack.placedIn,
396
+ hostDiscovery,
330
397
  recommendedPlacement: [
331
398
  "agent prompt",
332
399
  "workspace/IDENTITY.md",
333
400
  "workspace/USER.md",
334
401
  ],
335
402
  nextStep: ack.status === "acknowledged"
336
- ? "setup_already_acknowledged"
337
- : "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 } : {}),
338
410
  };
339
411
  if (includeSkill) {
340
412
  const skill = readPackagedSetupText("SKILL.md");
@@ -358,6 +430,7 @@ function buildSetupHintPayload(spine, input) {
358
430
  ok: true,
359
431
  command: "setup_hint",
360
432
  surfaceMode: "host_safe_carrier",
433
+ evidenceLevel: "contract_smoke",
361
434
  message: "Read the SKILL and guide as a friendly setup note, then place the guidance where the agent naturally checks its working anchors.",
362
435
  data,
363
436
  };
@@ -376,25 +449,48 @@ function buildSetupAckPayload(spine, input) {
376
449
  },
377
450
  };
378
451
  }
452
+ const placedIn = safeShortText(input?.placedIn, 160);
453
+ const placementProofRef = safeShortText(input?.placementProofRef, 320);
454
+ const writer = safeShortText(input?.writer, 80);
379
455
  const marker = {
456
+ schemaVersion: SETUP_ACK_SCHEMA_VERSION,
380
457
  acknowledgedAt: new Date().toISOString(),
381
458
  acceptedBy: safeShortText(input?.acceptedBy, 80) ?? "agent",
382
- placedIn: safeShortText(input?.placedIn, 160) ?? "unspecified",
459
+ placedIn: placedIn ?? "unspecified",
460
+ placementProofRef: placementProofRef ?? "",
383
461
  note: safeShortText(input?.note, 240),
384
462
  guideVersion: SETUP_GUIDE_VERSION,
385
463
  source: "second-nature-plugin",
386
464
  skillPath: "SKILL.md",
387
465
  guidePath: "agent-inner-guide.md",
466
+ writer: writer ?? "setup_ack_command",
388
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
+ }
389
482
  fs.mkdirSync(path.dirname(markerPath), { recursive: true });
390
483
  fs.writeFileSync(markerPath, `${JSON.stringify(marker, null, 2)}\n`, "utf-8");
484
+ const hostDiscovery = buildHostDiscoveryReport(input);
391
485
  return {
392
486
  ok: true,
393
487
  command: "setup_ack",
394
488
  surfaceMode: "host_safe_carrier",
395
- 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.",
396
491
  data: {
397
492
  markerPath,
493
+ hostDiscovery,
398
494
  ...marker,
399
495
  },
400
496
  };
@@ -1,13 +1,10 @@
1
1
  {
2
2
  "id": "second-nature",
3
3
  "name": "Second Nature",
4
- "version": "0.2.9",
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
- "onStartup": true,
8
- "onCapabilities": [
9
- "tool"
10
- ]
7
+ "onStartup": true
11
8
  },
12
9
  "contracts": {
13
10
  "commands": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haaaiawd/second-nature",
3
- "version": "0.2.9",
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
+ }
@@ -7,7 +7,7 @@ function mergeEvidenceRefs(...groups) {
7
7
  const out = [];
8
8
  for (const group of groups) {
9
9
  for (const ref of group) {
10
- const key = `${ref.kind}:${ref.id}`;
10
+ const key = `${ref.family}:${ref.id}`;
11
11
  if (!seen.has(key)) {
12
12
  seen.add(key);
13
13
  out.push(ref);
@@ -3,15 +3,10 @@
3
3
  *
4
4
  * Test coverage: tests/unit/cli/host-capability.test.ts, tests/integration/cli/host-capability-probe.test.ts
5
5
  */
6
+ import type { SourceRef } from "../../shared/types/v8-contracts.js";
7
+ export type { SourceRef } from "../../shared/types/v8-contracts.js";
6
8
  export type DeliveryCapabilityStatus = "target_available" | "target_none" | "channel_missing" | "host_api_unavailable" | "host_unsupported" | "unknown";
7
9
  export type CapabilityVerdict = "pass" | "fail" | "unknown" | "not_applicable";
8
- export interface SourceRef {
9
- id: string;
10
- kind: "platform_item" | "workspace_artifact" | "decision_record" | "user_anchor" | "connector_result" | "host_report" | "fallback_artifact";
11
- uri: string;
12
- excerptHash?: string;
13
- observedAt?: string;
14
- }
15
10
  export interface HostCapabilityDocReference {
16
11
  title: string;
17
12
  url: string;
@@ -1,6 +1 @@
1
- /**
2
- * Host capability probe contracts (cli-system v5 / ADR-007).
3
- *
4
- * Test coverage: tests/unit/cli/host-capability.test.ts, tests/integration/cli/host-capability-probe.test.ts
5
- */
6
1
  export {};