@haaaiawd/second-nature 0.1.16 → 0.1.18

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 (188) hide show
  1. package/index.js +855 -851
  2. package/openclaw.plugin.json +29 -29
  3. package/package.json +52 -52
  4. package/runtime/cli/commands/index.d.ts +14 -14
  5. package/runtime/cli/commands/index.js +193 -193
  6. package/runtime/cli/explain/explain-surface-subject.d.ts +8 -8
  7. package/runtime/cli/explain/explain-surface-subject.js +9 -9
  8. package/runtime/cli/explain/format-explanation.d.ts +12 -12
  9. package/runtime/cli/explain/format-explanation.js +12 -12
  10. package/runtime/cli/explain/resolve-subject.js +41 -41
  11. package/runtime/cli/host-capability/classify-delivery.d.ts +14 -14
  12. package/runtime/cli/host-capability/classify-delivery.js +20 -20
  13. package/runtime/cli/host-capability/probe-host-capability.d.ts +2 -2
  14. package/runtime/cli/host-capability/probe-host-capability.js +58 -58
  15. package/runtime/cli/host-capability/record-host-capability.d.ts +6 -6
  16. package/runtime/cli/host-capability/record-host-capability.js +14 -14
  17. package/runtime/cli/host-capability/types.d.ts +71 -71
  18. package/runtime/cli/host-capability/types.js +6 -6
  19. package/runtime/cli/host-smoke/run-host-smoke.d.ts +2 -2
  20. package/runtime/cli/host-smoke/run-host-smoke.js +40 -40
  21. package/runtime/cli/host-smoke/types.d.ts +35 -35
  22. package/runtime/cli/host-smoke/types.js +6 -6
  23. package/runtime/cli/index.js +58 -54
  24. package/runtime/cli/ops/heartbeat-surface.d.ts +38 -35
  25. package/runtime/cli/ops/heartbeat-surface.js +73 -71
  26. package/runtime/cli/ops/ops-router.d.ts +19 -16
  27. package/runtime/cli/ops/ops-router.js +89 -87
  28. package/runtime/cli/ops/show-operator-fallback.d.ts +13 -13
  29. package/runtime/cli/ops/show-operator-fallback.js +22 -22
  30. package/runtime/cli/ops/workspace-heartbeat-runner.d.ts +19 -10
  31. package/runtime/cli/ops/workspace-heartbeat-runner.js +39 -26
  32. package/runtime/cli/read-models/index.d.ts +29 -29
  33. package/runtime/cli/read-models/index.js +256 -256
  34. package/runtime/cli/read-models/operator-explain-map.d.ts +6 -6
  35. package/runtime/cli/read-models/operator-explain-map.js +10 -10
  36. package/runtime/cli/read-models/types.d.ts +79 -79
  37. package/runtime/cli/runtime/runtime-artifact-boundary.d.ts +28 -28
  38. package/runtime/cli/runtime/runtime-artifact-boundary.js +94 -94
  39. package/runtime/connectors/base/contract.d.ts +87 -87
  40. package/runtime/connectors/base/execution-policy.d.ts +47 -47
  41. package/runtime/connectors/base/execution-policy.js +82 -82
  42. package/runtime/connectors/base/index.d.ts +8 -8
  43. package/runtime/connectors/base/index.js +8 -8
  44. package/runtime/connectors/base/manifest.d.ts +64 -64
  45. package/runtime/connectors/base/manifest.js +86 -86
  46. package/runtime/connectors/base/map-life-evidence.d.ts +16 -16
  47. package/runtime/connectors/base/map-life-evidence.js +79 -79
  48. package/runtime/connectors/base/policy-layer.d.ts +29 -29
  49. package/runtime/connectors/base/policy-layer.js +198 -198
  50. package/runtime/connectors/base/route-planner.js +99 -99
  51. package/runtime/connectors/index.d.ts +5 -5
  52. package/runtime/connectors/index.js +5 -5
  53. package/runtime/connectors/near-real/near-real-connector-smoke.d.ts +19 -19
  54. package/runtime/connectors/near-real/near-real-connector-smoke.js +152 -152
  55. package/runtime/core/second-nature/heartbeat/heartbeat-executor.js +114 -114
  56. package/runtime/core/second-nature/heartbeat/heartbeat-loop.d.ts +63 -63
  57. package/runtime/core/second-nature/heartbeat/heartbeat-loop.js +139 -139
  58. package/runtime/core/second-nature/heartbeat/index.d.ts +8 -8
  59. package/runtime/core/second-nature/heartbeat/index.js +7 -7
  60. package/runtime/core/second-nature/heartbeat/run-heartbeat-cycle.d.ts +21 -21
  61. package/runtime/core/second-nature/heartbeat/run-heartbeat-cycle.js +35 -35
  62. package/runtime/core/second-nature/heartbeat/runtime-snapshot.d.ts +28 -28
  63. package/runtime/core/second-nature/heartbeat/runtime-snapshot.js +35 -35
  64. package/runtime/core/second-nature/heartbeat/signal.d.ts +42 -42
  65. package/runtime/core/second-nature/heartbeat/snapshot-builder.d.ts +51 -51
  66. package/runtime/core/second-nature/index.d.ts +22 -22
  67. package/runtime/core/second-nature/index.js +22 -22
  68. package/runtime/core/second-nature/orchestrator/effect-dispatcher.d.ts +100 -100
  69. package/runtime/core/second-nature/orchestrator/effect-dispatcher.js +144 -144
  70. package/runtime/core/second-nature/orchestrator/guard-layer.d.ts +8 -8
  71. package/runtime/core/second-nature/orchestrator/guard-layer.js +110 -110
  72. package/runtime/core/second-nature/orchestrator/intent-planner.d.ts +13 -13
  73. package/runtime/core/second-nature/orchestrator/intent-planner.js +199 -199
  74. package/runtime/core/second-nature/orchestrator/lease-manager.d.ts +14 -14
  75. package/runtime/core/second-nature/orchestrator/lease-manager.js +58 -58
  76. package/runtime/core/second-nature/outreach/build-outreach-draft-request.d.ts +6 -6
  77. package/runtime/core/second-nature/outreach/build-outreach-draft-request.js +63 -63
  78. package/runtime/core/second-nature/outreach/delivery-target.d.ts +26 -26
  79. package/runtime/core/second-nature/outreach/delivery-target.js +70 -70
  80. package/runtime/core/second-nature/outreach/dispatch-user-outreach.d.ts +38 -38
  81. package/runtime/core/second-nature/outreach/dispatch-user-outreach.js +119 -119
  82. package/runtime/core/second-nature/outreach/judge-input-from-snapshot.d.ts +7 -7
  83. package/runtime/core/second-nature/outreach/judge-input-from-snapshot.js +45 -45
  84. package/runtime/core/second-nature/outreach/judge-outreach.d.ts +40 -40
  85. package/runtime/core/second-nature/outreach/judge-outreach.js +121 -121
  86. package/runtime/core/second-nature/quiet/run-source-backed-quiet.d.ts +21 -21
  87. package/runtime/core/second-nature/quiet/run-source-backed-quiet.js +123 -123
  88. package/runtime/core/second-nature/rhythm/planner-rhythm-window.d.ts +15 -15
  89. package/runtime/core/second-nature/rhythm/planner-rhythm-window.js +52 -52
  90. package/runtime/core/second-nature/rhythm/policy-bridge.d.ts +19 -19
  91. package/runtime/core/second-nature/rhythm/policy-bridge.js +34 -34
  92. package/runtime/core/second-nature/runtime/service-entry.js +45 -45
  93. package/runtime/core/second-nature/types.d.ts +51 -51
  94. package/runtime/guidance/draft-outreach-message.d.ts +7 -7
  95. package/runtime/guidance/draft-outreach-message.js +42 -42
  96. package/runtime/guidance/evidence-guidance.d.ts +40 -40
  97. package/runtime/guidance/evidence-guidance.js +52 -52
  98. package/runtime/guidance/index.d.ts +11 -11
  99. package/runtime/guidance/index.js +11 -11
  100. package/runtime/guidance/outreach-draft-schema.d.ts +228 -228
  101. package/runtime/guidance/outreach-draft-schema.js +80 -80
  102. package/runtime/observability/audit/append-only-audit-store.d.ts +14 -14
  103. package/runtime/observability/audit/append-only-audit-store.js +21 -21
  104. package/runtime/observability/audit/audit-envelope.d.ts +51 -51
  105. package/runtime/observability/audit/audit-envelope.js +130 -130
  106. package/runtime/observability/audit/verify-audit-hash-chain.d.ts +23 -23
  107. package/runtime/observability/audit/verify-audit-hash-chain.js +83 -83
  108. package/runtime/observability/db/index.js +124 -124
  109. package/runtime/observability/db/schema/host-capability-reports.d.ts +180 -180
  110. package/runtime/observability/db/schema/host-capability-reports.js +12 -12
  111. package/runtime/observability/db/schema/index.d.ts +947 -947
  112. package/runtime/observability/db/schema/index.js +71 -71
  113. package/runtime/observability/index.d.ts +20 -19
  114. package/runtime/observability/index.js +19 -18
  115. package/runtime/observability/query/explain-query.d.ts +48 -48
  116. package/runtime/observability/query/explain-query.js +114 -114
  117. package/runtime/observability/query/export-audit-bundle.d.ts +22 -22
  118. package/runtime/observability/query/export-audit-bundle.js +27 -27
  119. package/runtime/observability/services/decision-ledger.d.ts +46 -46
  120. package/runtime/observability/services/decision-ledger.js +161 -161
  121. package/runtime/observability/services/governance-audit.d.ts +41 -41
  122. package/runtime/observability/services/governance-audit.js +163 -163
  123. package/runtime/observability/services/governance-plane-recorder.d.ts +47 -47
  124. package/runtime/observability/services/governance-plane-recorder.js +55 -55
  125. package/runtime/observability/services/lived-experience-audit.d.ts +97 -97
  126. package/runtime/observability/services/lived-experience-audit.js +162 -162
  127. package/runtime/observability/services/runtime-decision-recorder.d.ts +29 -0
  128. package/runtime/observability/services/runtime-decision-recorder.js +94 -0
  129. package/runtime/storage/bootstrap/native-sqlite-probe.d.ts +7 -7
  130. package/runtime/storage/bootstrap/native-sqlite-probe.js +28 -28
  131. package/runtime/storage/bootstrap/repair-gate.d.ts +17 -17
  132. package/runtime/storage/bootstrap/repair-gate.js +71 -71
  133. package/runtime/storage/bootstrap/storage-mode-smoke.d.ts +38 -38
  134. package/runtime/storage/bootstrap/storage-mode-smoke.js +85 -85
  135. package/runtime/storage/db/index.js +154 -154
  136. package/runtime/storage/db/schema/delivery-attempts.d.ts +199 -199
  137. package/runtime/storage/db/schema/delivery-attempts.js +13 -13
  138. package/runtime/storage/db/schema/index.d.ts +9 -9
  139. package/runtime/storage/db/schema/index.js +9 -9
  140. package/runtime/storage/db/schema/life-evidence-index.d.ts +161 -161
  141. package/runtime/storage/db/schema/life-evidence-index.js +11 -11
  142. package/runtime/storage/db/schema/operator-fallback-artifacts.d.ts +161 -161
  143. package/runtime/storage/db/schema/operator-fallback-artifacts.js +11 -11
  144. package/runtime/storage/db/schema/policies.d.ts +98 -98
  145. package/runtime/storage/db/schema/policies.js +8 -8
  146. package/runtime/storage/delivery/query-delivery-attempts.d.ts +3 -3
  147. package/runtime/storage/delivery/query-delivery-attempts.js +32 -32
  148. package/runtime/storage/delivery/types.d.ts +27 -27
  149. package/runtime/storage/delivery/types.js +1 -1
  150. package/runtime/storage/delivery/write-delivery-attempt.d.ts +6 -6
  151. package/runtime/storage/delivery/write-delivery-attempt.js +36 -36
  152. package/runtime/storage/fallback/load-operator-fallback.d.ts +14 -14
  153. package/runtime/storage/fallback/load-operator-fallback.js +47 -47
  154. package/runtime/storage/fallback/operator-fallback-types.d.ts +9 -9
  155. package/runtime/storage/fallback/operator-fallback-types.js +1 -1
  156. package/runtime/storage/fallback/operator-fallback-view.d.ts +11 -11
  157. package/runtime/storage/fallback/operator-fallback-view.js +1 -1
  158. package/runtime/storage/fallback/write-operator-fallback.d.ts +6 -6
  159. package/runtime/storage/fallback/write-operator-fallback.js +21 -21
  160. package/runtime/storage/index.d.ts +37 -37
  161. package/runtime/storage/index.js +30 -30
  162. package/runtime/storage/life-evidence/append-life-evidence.d.ts +7 -7
  163. package/runtime/storage/life-evidence/append-life-evidence.js +64 -64
  164. package/runtime/storage/life-evidence/types.d.ts +45 -45
  165. package/runtime/storage/life-evidence/types.js +6 -6
  166. package/runtime/storage/quiet/persist-quiet-artifact.d.ts +7 -7
  167. package/runtime/storage/quiet/persist-quiet-artifact.js +22 -22
  168. package/runtime/storage/quiet/quiet-artifact-types.d.ts +18 -18
  169. package/runtime/storage/quiet/quiet-artifact-types.js +1 -1
  170. package/runtime/storage/quiet/quiet-artifact-writer.d.ts +15 -15
  171. package/runtime/storage/quiet/quiet-artifact-writer.js +56 -56
  172. package/runtime/storage/repositories/credential-repository.js +30 -30
  173. package/runtime/storage/rhythm/rhythm-policy-snapshot.d.ts +10 -10
  174. package/runtime/storage/rhythm/rhythm-policy-snapshot.js +34 -34
  175. package/runtime/storage/services/credential-vault.d.ts +13 -13
  176. package/runtime/storage/services/credential-vault.js +116 -116
  177. package/runtime/storage/snapshots/continuity-snapshot.d.ts +9 -9
  178. package/runtime/storage/snapshots/continuity-snapshot.js +41 -41
  179. package/runtime/storage/snapshots/life-evidence-snapshot.d.ts +6 -6
  180. package/runtime/storage/snapshots/life-evidence-snapshot.js +114 -114
  181. package/runtime/storage/snapshots/types.d.ts +58 -58
  182. package/runtime/storage/snapshots/types.js +1 -1
  183. package/runtime/storage/state-api.js +104 -104
  184. package/runtime/storage/user-interest/load-user-interest-snapshot.d.ts +2 -2
  185. package/runtime/storage/user-interest/load-user-interest-snapshot.js +150 -150
  186. package/runtime/storage/user-interest/types.d.ts +25 -25
  187. package/runtime/storage/user-interest/types.js +1 -1
  188. package/workspace-ops-bridge.js +81 -80
package/index.js CHANGED
@@ -1,851 +1,855 @@
1
- /**
2
- * Host-safe Second Nature plugin surface.
3
- *
4
- * Core logic:
5
- * - keep register(api) synchronous so OpenClaw captures services/command/tool before return
6
- * - avoid importing CLI/runtime DB modules at module-evaluation time because the packaged
7
- * runtime graph currently contains async sql.js bootstrap that breaks vm sandbox loading
8
- * - expose a minimal in-memory activation spine so status/lifecycle stay truthful even when
9
- * the full workspace runtime is not loaded inside the host
10
- *
11
- * Dependencies:
12
- * - only imports runtime lifecycle/service modules that are synchronous at load time
13
- *
14
- * Boundaries:
15
- * - read-only operator flows stay available through command/tool surface
16
- * - structured mutating flows such as policy set / credential verify remain unavailable here
17
- * - full evidence-backed workspace runtime can be reintroduced later behind a host-safe boundary
18
- *
19
- * Plugin classification (verified against OpenClaw 2026.5.4 internals, see
20
- * docs/validation/openclaw-plugin-classification.md and the explore reports
21
- * dated 2026-05-06):
22
- * - Second Nature is a TOOL plugin (exposes `second_nature_ops` to agent
23
- * sessions). It is intentionally NOT a channel/provider/context-engine.
24
- * - OpenClaw's `loadGatewayStartupPluginPlan` only loads plugins that opt in
25
- * via `manifest.activation.onStartup === true`, occupy a configured slot
26
- * (channel/contextEngine/provider), or declare an explicit hook intent. A
27
- * tool-only plugin without `activation.onStartup` will be enabled in the
28
- * registry yet never loaded by the gateway daemon — register(api) only fires
29
- * inside the `openclaw plugins enable` CLI process, which produces the
30
- * illusion of a working plugin while agent sessions see no tool. We hit
31
- * exactly that on 2026-05-06; the fix lives in plugin/openclaw.plugin.json
32
- * under the `activation` block.
33
- * - `Shape: non-capability` reported by `openclaw plugins info` is EXPECTED
34
- * for this plugin. OpenClaw counts capabilities only across cli-backend /
35
- * text-inference / speech / realtime-* / media-understanding /
36
- * image-generation / web-search / agent-harness / context-engine / channel.
37
- * Tool/command/service contributions never bump that count. Pretending to
38
- * be a context engine with a stub factory just to flip the label would lie
39
- * to the host (ContextEngine.ingest/assemble/compact get called for real).
40
- * When Second Nature ships a genuine context-engine layer in a future
41
- * release, the shape will move to plain-capability honestly.
42
- *
43
- * OpenClaw operator norm (T1.1.4 / T1.1.5): set `SECOND_NATURE_WORKSPACE_ROOT` or tool `workspaceRoot` to the
44
- * **same absolute path** as the OpenClaw **agent workspace** (default `~/.openclaw/workspace`, or
45
- * `agents.defaults.workspace` in `~/.openclaw/openclaw.json`). Do **not** infer that root from the plugin
46
- * install directory. With **sandbox** or **per-agent workspaces**, use the path where `data/state.db` and
47
- * `workspace/` anchors actually live. See `explore/reports/2026-05-04_openclaw-plugin-install-vs-workspace-root.md`.
48
- *
49
- * Test coverage:
50
- * - tests/integration/cli/plugin-runtime-registration.test.ts
51
- * - tests/integration/cli/plugin-packaging-walkthrough.test.ts
52
- * - tests/integration/cli/plugin-workspace-ops-bridge.test.ts (T1.1.4 / CH-13 matrix, T1.1.5 ops docs cross-ref)
53
- */
54
- import { startRuntimeService, } from "./runtime/core/second-nature/runtime/service-entry.js";
55
- import { getLifecycleState, recordRegistration, } from "./runtime/core/second-nature/runtime/lifecycle-service.js";
56
- import { openWorkspaceOpsBridge } from "./workspace-ops-bridge.js";
57
- // definePluginEntry is OpenClaw's canonical factory for non-channel plugins
58
- // (provider/tool/command/service/memory/context-engine). At runtime it returns
59
- // a plain options object; it does NOT add a brand symbol — earlier debugging
60
- // rounds wrongly assumed the factory was the "plain-capability" marker. The
61
- // real classification happens via manifest fields (see file header). We still
62
- // use the factory because it is the documented, supported entry shape, and
63
- // keeping it future-proof against SDK option-processing changes.
64
- //
65
- // IMPORTANT — keep this a STATIC import. The packaged runtime is loaded inside
66
- // OpenClaw's vm sandbox, which rejects top-level await (manifests as
67
- // "SyntaxError: Unexpected identifier 'Promise'" at host load time). The same
68
- // constraint applies to the sql.js async bootstrap noted in the file header.
69
- // In production the host always provides `openclaw` as a sibling module under
70
- // ~/.openclaw/npm/node_modules/, so this resolves synchronously. Locally,
71
- // `openclaw` is declared as a devDependency so build and tests resolve via
72
- // the same import path.
73
- import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
74
- // Stderr sentinels make daemon load-path observable in `gateway.log`. Three
75
- // lines should appear at startup: "module evaluated", "register() entered ...",
76
- // "register() completed". Their absence after `openclaw gateway run` proves
77
- // the daemon never reached this entry — typically a manifest activation gap.
78
- process.stderr.write("[second-nature] module evaluated\n");
79
- const INTERNAL_RUNTIME_TRACE_PREFIX = "sn-runtime-";
80
- const HOST_SAFE_LIMITATION_MESSAGE = "Host-safe plugin package keeps synchronous register/load semantics, but mutating workspace runtime flows remain unavailable here.";
81
- let activationSpine = null;
82
- /** T1.1.4 — lazily opened full read bridge; closed when workspace root / resolution changes. */
83
- let workspaceOpsBridge = null;
84
- function disposeWorkspaceOpsBridge() {
85
- if (workspaceOpsBridge) {
86
- workspaceOpsBridge.close();
87
- workspaceOpsBridge = null;
88
- }
89
- }
90
- const WORKSPACE_BRIDGE_COMMANDS = new Set([
91
- "status",
92
- "quiet",
93
- "report",
94
- "session",
95
- "explain",
96
- "heartbeat_check",
97
- "fallback",
98
- "storage_smoke",
99
- ]);
100
- function isWorkspaceBridgeCommand(command, input) {
101
- if (command === "credential") {
102
- const action = typeof input?.action === "string" ? input.action : "show";
103
- return action !== "verify";
104
- }
105
- return WORKSPACE_BRIDGE_COMMANDS.has(command);
106
- }
107
- async function ensureWorkspaceOpsBridge(spine) {
108
- const root = spine.workspaceRootContext.runtimeRoot;
109
- if (workspaceOpsBridge?.root === root) {
110
- return { ok: true, dispatch: workspaceOpsBridge.dispatch };
111
- }
112
- disposeWorkspaceOpsBridge();
113
- const opened = await openWorkspaceOpsBridge(root);
114
- if (!opened.ok) {
115
- return opened;
116
- }
117
- workspaceOpsBridge = { root, close: opened.close, dispatch: opened.dispatch };
118
- return { ok: true, dispatch: opened.dispatch };
119
- }
120
- async function routeSecondNatureCommand(spine, command, input) {
121
- const wr = spine.workspaceRootContext;
122
- const useBridge = wr.resolution !== "unknown" && isWorkspaceBridgeCommand(command, input);
123
- if (useBridge) {
124
- const bridge = await ensureWorkspaceOpsBridge(spine);
125
- if (!bridge.ok) {
126
- return {
127
- ok: false,
128
- surfaceMode: "host_safe_carrier",
129
- workspaceReadModelsEvaluated: false,
130
- message: HOST_SAFE_LIMITATION_MESSAGE,
131
- error: bridge.error,
132
- data: {
133
- workspaceRootResolution: wr.resolution,
134
- bridgeAttempted: true,
135
- declaredRoot: wr.declaredRoot,
136
- },
137
- };
138
- }
139
- return (await bridge.dispatch(command, input));
140
- }
141
- const def = spine.router.resolve(command);
142
- if (!def) {
143
- return { ok: false, message: `Unknown Second Nature command: ${command}` };
144
- }
145
- return def.execute(input);
146
- }
147
- function resolveWorkspaceRoot(toolWorkspaceRoot) {
148
- const env = process.env.SECOND_NATURE_WORKSPACE_ROOT?.trim();
149
- if (env) {
150
- return { resolution: "env", declaredRoot: env, runtimeRoot: env };
151
- }
152
- const tool = toolWorkspaceRoot?.trim();
153
- if (tool) {
154
- return { resolution: "tool_args", declaredRoot: tool, runtimeRoot: tool };
155
- }
156
- return { resolution: "unknown", declaredRoot: undefined, runtimeRoot: process.cwd() };
157
- }
158
- function syncWorkspaceRootFromTool(spine, toolWorkspaceRoot) {
159
- const next = resolveWorkspaceRoot(toolWorkspaceRoot);
160
- const prev = spine.workspaceRootContext;
161
- const changed = next.runtimeRoot !== prev.runtimeRoot || next.resolution !== prev.resolution;
162
- if (changed) {
163
- disposeWorkspaceOpsBridge();
164
- }
165
- spine.workspaceRootContext = next;
166
- if (changed) {
167
- spine.runtimeHandle = startRuntimeService({ workspaceRoot: next.runtimeRoot });
168
- }
169
- }
170
- function trimRuntimeEvidence(spine) {
171
- if (spine.runtimeEvidence.length > 12) {
172
- spine.runtimeEvidence.splice(0, spine.runtimeEvidence.length - 12);
173
- }
174
- }
175
- function latestRuntimeEvidence(spine) {
176
- return spine.runtimeEvidence[spine.runtimeEvidence.length - 1];
177
- }
178
- function createUnavailableActionError(code, message, requiredUserInput, nextStep) {
179
- return {
180
- ok: false,
181
- error: {
182
- code,
183
- message,
184
- requiredUserInput,
185
- nextStep,
186
- },
187
- message: HOST_SAFE_LIMITATION_MESSAGE,
188
- };
189
- }
190
- function parseExplainSubject(subjectRaw) {
191
- const trimmed = subjectRaw.trim();
192
- if (!trimmed) {
193
- throw new Error("explain_subject_invalid");
194
- }
195
- const separatorIndex = trimmed.indexOf(":");
196
- if (separatorIndex === -1) {
197
- throw new Error("explain_subject_requires_id");
198
- }
199
- const kind = trimmed.slice(0, separatorIndex).trim();
200
- const id = trimmed.slice(separatorIndex + 1).trim();
201
- if (!id) {
202
- throw new Error("explain_subject_requires_id");
203
- }
204
- switch (kind) {
205
- case "decision":
206
- return { subjectType: "decision", subjectId: id };
207
- case "platform":
208
- case "platform-selection":
209
- return { subjectType: "platform-selection", subjectId: id };
210
- case "outreach":
211
- return { subjectType: "outreach", subjectId: id };
212
- case "soul":
213
- case "soul-change":
214
- return { subjectType: "soul-change", subjectId: id };
215
- case "fallback":
216
- return { subjectType: "fallback", subjectId: id };
217
- case "probe":
218
- return { subjectType: "probe", subjectId: id };
219
- case "report":
220
- return { subjectType: "report", subjectId: id };
221
- case "delivery":
222
- return { subjectType: "delivery", subjectId: id };
223
- case "source":
224
- case "source_ref":
225
- return { subjectType: "source_ref", subjectId: id };
226
- default:
227
- throw new Error("explain_subject_unsupported");
228
- }
229
- }
230
- function buildStatusPayload(spine) {
231
- const runtimeEvidence = latestRuntimeEvidence(spine);
232
- const updatedAt = runtimeEvidence?.createdAt ?? new Date(spine.lifecycleState.lastChangedAt).toISOString();
233
- const wr = spine.workspaceRootContext;
234
- const needsRootHint = wr.resolution === "unknown";
235
- return {
236
- ok: false,
237
- surfaceMode: "host_safe_carrier",
238
- workspaceReadModelsEvaluated: false,
239
- message: HOST_SAFE_LIMITATION_MESSAGE,
240
- error: {
241
- code: "WORKSPACE_READ_SURFACE_UNAVAILABLE",
242
- message: "Aggregated status requires workspace state; the host-safe plugin does not load persisted read models on this surface.",
243
- requiredUserInput: needsRootHint ? ["SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot"] : [],
244
- nextStep: "run_workspace_second_nature_cli_or_full_runtime_package",
245
- },
246
- data: {
247
- workspaceRootResolution: wr.resolution,
248
- carrier: {
249
- host: "openclaw-plugin",
250
- serviceStatus: spine.runtimeHandle.ready ? "running" : "idle",
251
- updatedAt,
252
- lastRuntimeTraceId: runtimeEvidence?.traceId,
253
- },
254
- },
255
- };
256
- }
257
- function buildQuietPayload(spine, scope) {
258
- const wr = spine.workspaceRootContext;
259
- return {
260
- ok: false,
261
- surfaceMode: "host_safe_carrier",
262
- workspaceReadModelsEvaluated: false,
263
- message: HOST_SAFE_LIMITATION_MESSAGE,
264
- error: {
265
- code: "QUIET_READ_SURFACE_UNAVAILABLE",
266
- message: "Quiet read surface requires workspace runtime; not evaluated in host-safe carrier mode.",
267
- requiredUserInput: wr.resolution === "unknown" ? ["SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot"] : [],
268
- nextStep: "run_workspace_second_nature_cli_or_full_runtime_package",
269
- },
270
- data: {
271
- scope,
272
- evaluated: false,
273
- unavailableReason: "host_safe_carrier_no_workspace_db",
274
- workspaceRootResolution: wr.resolution,
275
- },
276
- };
277
- }
278
- function buildReportPayload(spine, day) {
279
- const wr = spine.workspaceRootContext;
280
- return {
281
- ok: false,
282
- surfaceMode: "host_safe_carrier",
283
- workspaceReadModelsEvaluated: false,
284
- message: HOST_SAFE_LIMITATION_MESSAGE,
285
- error: {
286
- code: "REPORT_READ_SURFACE_UNAVAILABLE",
287
- message: "Daily report artifacts require workspace runtime.",
288
- requiredUserInput: wr.resolution === "unknown" ? ["SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot"] : [],
289
- nextStep: "run_workspace_second_nature_cli_or_full_runtime_package",
290
- },
291
- data: {
292
- evaluated: false,
293
- unavailableReason: "host_safe_carrier_no_workspace_db",
294
- day: day && day.trim() ? day : new Date().toISOString().slice(0, 10),
295
- workspaceRootResolution: wr.resolution,
296
- },
297
- };
298
- }
299
- function buildSessionPayload(spine, sessionId) {
300
- if (!sessionId) {
301
- return {
302
- ok: false,
303
- error: {
304
- code: "MISSING_SESSION_ID",
305
- message: "session show requires sessionId",
306
- requiredUserInput: ["session_id"],
307
- nextStep: "reinvoke_session_with_session_id",
308
- },
309
- };
310
- }
311
- const wr = spine.workspaceRootContext;
312
- return {
313
- ok: false,
314
- surfaceMode: "host_safe_carrier",
315
- workspaceReadModelsEvaluated: false,
316
- message: HOST_SAFE_LIMITATION_MESSAGE,
317
- error: {
318
- code: "SESSION_READ_SURFACE_UNAVAILABLE",
319
- message: "Session analytics require workspace state database.",
320
- requiredUserInput: wr.resolution === "unknown" ? ["SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot"] : [],
321
- nextStep: "run_workspace_second_nature_cli_or_full_runtime_package",
322
- },
323
- data: {
324
- requestedSessionId: sessionId,
325
- evaluated: false,
326
- unavailableReason: "host_safe_carrier_no_workspace_db",
327
- workspaceRootResolution: wr.resolution,
328
- },
329
- };
330
- }
331
- function buildCredentialPayload(spine, platformId) {
332
- const wr = spine.workspaceRootContext;
333
- return {
334
- ok: false,
335
- surfaceMode: "host_safe_carrier",
336
- workspaceReadModelsEvaluated: false,
337
- message: HOST_SAFE_LIMITATION_MESSAGE,
338
- error: {
339
- code: "CREDENTIAL_READ_SURFACE_UNAVAILABLE",
340
- message: "Credential inspection requires workspace runtime on this surface.",
341
- requiredUserInput: wr.resolution === "unknown" ? ["SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot"] : [],
342
- nextStep: "run_workspace_second_nature_cli_or_full_runtime_package",
343
- },
344
- data: {
345
- platformId: platformId && platformId.trim() ? platformId : undefined,
346
- evaluated: false,
347
- unavailableReason: "host_safe_carrier_no_workspace_db",
348
- workspaceRootResolution: wr.resolution,
349
- },
350
- };
351
- }
352
- function buildExplainPayload(spine, subjectRaw) {
353
- if (!subjectRaw?.trim()) {
354
- return {
355
- ok: false,
356
- error: {
357
- code: "MISSING_EXPLAIN_SUBJECT",
358
- message: "explain requires subject",
359
- requiredUserInput: ["subject"],
360
- nextStep: "reinvoke_explain_with_subject",
361
- },
362
- };
363
- }
364
- let subject;
365
- try {
366
- subject = parseExplainSubject(subjectRaw);
367
- }
368
- catch (error) {
369
- const code = error.message;
370
- if (code === "explain_subject_requires_id") {
371
- return createUnavailableActionError("EXPLAIN_SUBJECT_REQUIRES_ID", "subject must include identifier", ["subject"], "reinvoke_explain_with_supported_subject");
372
- }
373
- if (code === "explain_subject_unsupported") {
374
- return createUnavailableActionError("EXPLAIN_SUBJECT_UNSUPPORTED", "supported subjects include decision:, platform:, outreach:, soul:, fallback:, delivery:, probe:, report:, source:", ["subject"], "reinvoke_explain_with_supported_subject");
375
- }
376
- return createUnavailableActionError("EXPLAIN_SUBJECT_INVALID", "invalid explain subject", ["subject"], "reinvoke_explain_with_supported_subject");
377
- }
378
- const wr = spine.workspaceRootContext;
379
- return {
380
- ok: false,
381
- surfaceMode: "host_safe_carrier",
382
- workspaceReadModelsEvaluated: false,
383
- message: HOST_SAFE_LIMITATION_MESSAGE,
384
- error: {
385
- code: "EXPLAIN_READ_SURFACE_UNAVAILABLE",
386
- message: "Evidence-backed explain requires persisted workspace read models; host-safe carrier did not evaluate operator explain (CH-11-02).",
387
- requiredUserInput: wr.resolution === "unknown" ? ["SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot"] : [],
388
- nextStep: "run_workspace_second_nature_cli_or_full_runtime_package",
389
- },
390
- data: {
391
- subjectType: subject.subjectType,
392
- evaluated: false,
393
- workspaceRootResolution: wr.resolution,
394
- },
395
- };
396
- }
397
- async function buildStorageSmokePayload(input) {
398
- try {
399
- const mod = await import("./runtime/storage/bootstrap/storage-mode-smoke.js");
400
- const runRepairFixture = Boolean(input?.runRepairFixture);
401
- const workspaceRoot = typeof input?.workspaceRoot === "string" ? input.workspaceRoot : undefined;
402
- const data = await mod.runStorageModeSmoke({ runRepairFixture, workspaceRoot });
403
- return { ok: true, data };
404
- }
405
- catch (error) {
406
- return {
407
- ok: false,
408
- message: error instanceof Error ? error.message : String(error),
409
- error: {
410
- code: "STORAGE_SMOKE_LOAD_FAILED",
411
- message: "Could not load packaged storage-mode smoke module",
412
- nextStep: "rebuild_plugin_runtime_package",
413
- },
414
- };
415
- }
416
- }
417
- function buildFallbackHostSafePayload(ref) {
418
- if (!ref?.trim()) {
419
- return {
420
- ok: false,
421
- error: {
422
- code: "MISSING_FALLBACK_REF",
423
- message: "fallback requires ref (e.g. fallback:…)",
424
- requiredUserInput: ["ref"],
425
- nextStep: "reinvoke_with_ref",
426
- },
427
- };
428
- }
429
- return createUnavailableActionError("HOST_SAFE_FALLBACK_VIEW_UNAVAILABLE", "Operator fallback view requires workspace state database; host-safe plugin cannot read persisted fallback artifacts.", ["ref"], "run_workspace_second_nature_cli_or_full_runtime_package");
430
- }
431
- function isProbeOnlyInput(input) {
432
- const v = input?.probeOnly;
433
- return v === true || v === "true" || v === 1 || v === "1";
434
- }
435
- function buildHeartbeatCheckPayload(spine, input) {
436
- const runtimeEvidence = latestRuntimeEvidence(spine);
437
- const updatedAt = runtimeEvidence?.createdAt ?? new Date(spine.lifecycleState.lastChangedAt).toISOString();
438
- const timestamp = typeof input?.timestamp === "string" && input.timestamp.trim().length > 0 ? input.timestamp : updatedAt;
439
- const wr = spine.workspaceRootContext;
440
- if (isProbeOnlyInput(input)) {
441
- return {
442
- ok: true,
443
- status: "heartbeat_ok",
444
- surfaceMode: "capability_probe",
445
- reasons: ["probe_only"],
446
- livedExperienceLoopClaimed: false,
447
- scope: "rhythm",
448
- trigger: "heartbeat_bridge",
449
- message: "Capability probe only on the host-safe carrier surface; does not claim a full lived-experience decision loop.",
450
- data: {
451
- workspaceRootResolution: wr.resolution,
452
- runtime: {
453
- host: "openclaw-plugin",
454
- serviceStatus: spine.runtimeHandle.ready ? "running" : "idle",
455
- updatedAt,
456
- },
457
- surface: {
458
- tool: "second_nature_ops",
459
- command: "second-nature heartbeat_check",
460
- },
461
- bridge: {
462
- timestamp,
463
- probeOnly: true,
464
- sessionContextProvided: typeof input?.sessionContext === "string" && input.sessionContext.trim().length > 0,
465
- heartbeatChecklistProvided: typeof input?.heartbeatChecklist === "string" && input.heartbeatChecklist.trim().length > 0,
466
- serviceEntryMode: "capability_probe",
467
- },
468
- },
469
- };
470
- }
471
- return {
472
- ok: true,
473
- status: "runtime_carrier_only",
474
- surfaceMode: "host_safe_carrier",
475
- livedExperienceLoopClaimed: false,
476
- scope: "rhythm",
477
- trigger: "heartbeat_bridge",
478
- reasons: ["runtime_carrier_only", "host_safe_bridge_ack"],
479
- nextAction: "continue_carrier_surface_only",
480
- message: "Packaged carrier acknowledged this heartbeat round. This is not a full lived-experience decision loop; use the workspace CLI when read models are required.",
481
- data: {
482
- workspaceRootResolution: wr.resolution,
483
- runtime: {
484
- host: "openclaw-plugin",
485
- serviceStatus: spine.runtimeHandle.ready ? "running" : "idle",
486
- updatedAt,
487
- },
488
- surface: {
489
- tool: "second_nature_ops",
490
- command: "second-nature heartbeat_check",
491
- },
492
- bridge: {
493
- timestamp,
494
- sessionContextProvided: typeof input?.sessionContext === "string" && input.sessionContext.trim().length > 0,
495
- heartbeatChecklistProvided: typeof input?.heartbeatChecklist === "string" && input.heartbeatChecklist.trim().length > 0,
496
- serviceEntryMode: "runtime_carrier_only",
497
- },
498
- },
499
- };
500
- }
501
- function createHostSafeRouter(spine) {
502
- const notImplemented = async (command) => ({
503
- ok: false,
504
- command,
505
- message: HOST_SAFE_LIMITATION_MESSAGE,
506
- });
507
- const commands = [
508
- {
509
- name: "status",
510
- description: "Show aggregated Second Nature status",
511
- execute: async () => buildStatusPayload(spine),
512
- },
513
- {
514
- name: "policy",
515
- description: "Write or inspect policy state",
516
- execute: async (input) => {
517
- const action = typeof input?.action === "string" ? input.action : "show";
518
- if (action === "set") {
519
- return createUnavailableActionError("HOST_SAFE_POLICY_SET_UNAVAILABLE", "policy set is unavailable in the host-safe plugin package", ["social_daily_limit", "quiet_enabled"], "run_workspace_runtime_or_reinstall_full_build");
520
- }
521
- return notImplemented("policy");
522
- },
523
- },
524
- {
525
- name: "credential",
526
- description: "Inspect or recover credential state",
527
- execute: async (input) => {
528
- const action = typeof input?.action === "string" ? input.action : "show";
529
- if (action === "verify") {
530
- return createUnavailableActionError("HOST_SAFE_CREDENTIAL_VERIFY_UNAVAILABLE", "credential verify is unavailable in the host-safe plugin package", ["verification_answer"], "run_workspace_runtime_or_reinstall_full_build");
531
- }
532
- const platformId = typeof input?.platformId === "string" ? input.platformId : undefined;
533
- return buildCredentialPayload(spine, platformId);
534
- },
535
- },
536
- {
537
- name: "quiet",
538
- description: "Inspect Quiet lifecycle state",
539
- execute: async (input) => {
540
- const scope = typeof input?.scope === "string" ? input.scope : undefined;
541
- return buildQuietPayload(spine, scope);
542
- },
543
- },
544
- {
545
- name: "report",
546
- description: "Show daily report artifacts",
547
- execute: async (input) => {
548
- const day = typeof input?.day === "string" ? input.day : undefined;
549
- return buildReportPayload(spine, day);
550
- },
551
- },
552
- {
553
- name: "session",
554
- description: "Inspect continuity session details",
555
- execute: async (input) => {
556
- const sessionId = typeof input?.sessionId === "string" ? input.sessionId : undefined;
557
- return buildSessionPayload(spine, sessionId);
558
- },
559
- },
560
- {
561
- name: "audit",
562
- description: "Inspect audit and evidence views",
563
- execute: async () => notImplemented("audit"),
564
- },
565
- {
566
- name: "explain",
567
- description: "Answer why-question explain requests",
568
- execute: async (input) => {
569
- const subject = typeof input?.subject === "string" ? input.subject : undefined;
570
- return buildExplainPayload(spine, subject);
571
- },
572
- },
573
- {
574
- name: "heartbeat_check",
575
- description: "Acknowledge the shipping heartbeat bridge round",
576
- execute: async (input) => buildHeartbeatCheckPayload(spine, input),
577
- },
578
- {
579
- name: "fallback",
580
- description: "Operator-visible delivery fallback view (full workspace runtime required)",
581
- execute: async (input) => {
582
- const ref = typeof input?.ref === "string" ? input.ref.trim() : undefined;
583
- return buildFallbackHostSafePayload(ref);
584
- },
585
- },
586
- {
587
- name: "storage_smoke",
588
- description: "T4.1.4 storage mode smoke report (sql.js vs native probe)",
589
- execute: async (input) => buildStorageSmokePayload(input),
590
- },
591
- ];
592
- return {
593
- commands,
594
- resolve(name) {
595
- return commands.find((command) => command.name === name);
596
- },
597
- };
598
- }
599
- function createActivationSpine() {
600
- const workspaceRootContext = resolveWorkspaceRoot(undefined);
601
- const spine = {
602
- router: undefined,
603
- runtimeHandle: startRuntimeService({ workspaceRoot: workspaceRootContext.runtimeRoot }),
604
- lifecycleState: getLifecycleState(),
605
- serviceStartRecorded: false,
606
- runtimeEvidence: [],
607
- workspaceRootContext,
608
- };
609
- spine.router = createHostSafeRouter(spine);
610
- return spine;
611
- }
612
- function ensureActivationSpine() {
613
- if (activationSpine) {
614
- return activationSpine;
615
- }
616
- activationSpine = createActivationSpine();
617
- return activationSpine;
618
- }
619
- function recordRuntimeEvidence(spine, origin) {
620
- if (origin === "service_start" && spine.serviceStartRecorded) {
621
- return;
622
- }
623
- if (origin === "service_start") {
624
- spine.serviceStartRecorded = true;
625
- }
626
- spine.runtimeEvidence.push({
627
- traceId: `${INTERNAL_RUNTIME_TRACE_PREFIX}${origin}-${spine.lifecycleState.registerCount}-${Date.now()}`,
628
- capability: origin === "register"
629
- ? spine.lifecycleState.registerCount === 1
630
- ? "runtime.activate"
631
- : "runtime.reload"
632
- : "runtime.heartbeat",
633
- origin,
634
- createdAt: new Date().toISOString(),
635
- status: "succeeded",
636
- });
637
- trimRuntimeEvidence(spine);
638
- }
639
- function refreshRegistrationState() {
640
- const spine = ensureActivationSpine();
641
- const workspaceRootContext = resolveWorkspaceRoot(undefined);
642
- const prev = spine.workspaceRootContext;
643
- const changed = workspaceRootContext.runtimeRoot !== prev.runtimeRoot || workspaceRootContext.resolution !== prev.resolution;
644
- if (changed) {
645
- disposeWorkspaceOpsBridge();
646
- }
647
- spine.workspaceRootContext = workspaceRootContext;
648
- spine.runtimeHandle = startRuntimeService({ workspaceRoot: workspaceRootContext.runtimeRoot });
649
- spine.lifecycleState = recordRegistration();
650
- spine.serviceStartRecorded = false;
651
- recordRuntimeEvidence(spine, "register");
652
- return spine;
653
- }
654
- function parseCommandInput(rawArgs) {
655
- const tokens = rawArgs?.trim().split(/\s+/).filter(Boolean) ?? [];
656
- if (tokens.length === 0) {
657
- return {
658
- ok: false,
659
- result: { ok: false, message: "Missing command argument." },
660
- };
661
- }
662
- const [command, ...rest] = tokens;
663
- if (command === "policy" && rest[0] === "set") {
664
- return {
665
- ok: false,
666
- result: {
667
- ok: false,
668
- command,
669
- message: "policy set requires structured args; use second_nature_ops instead.",
670
- },
671
- };
672
- }
673
- if (command === "credential" && rest[0] === "verify") {
674
- return {
675
- ok: false,
676
- result: {
677
- ok: false,
678
- command,
679
- message: "credential verify requires structured args; use second_nature_ops instead.",
680
- },
681
- };
682
- }
683
- switch (command) {
684
- case "status":
685
- case "quiet":
686
- return {
687
- ok: true,
688
- command,
689
- input: rest.length > 0 ? { scope: rest.join(" ") } : undefined,
690
- };
691
- case "report":
692
- return {
693
- ok: true,
694
- command,
695
- input: rest[0] ? { day: rest[0] } : undefined,
696
- };
697
- case "session":
698
- return {
699
- ok: true,
700
- command,
701
- input: rest[0] ? { sessionId: rest[0] } : undefined,
702
- };
703
- case "credential":
704
- return {
705
- ok: true,
706
- command,
707
- input: rest[0] ? { platformId: rest[0] } : undefined,
708
- };
709
- case "heartbeat_check":
710
- return {
711
- ok: true,
712
- command,
713
- input: rest.length > 0
714
- ? {
715
- timestamp: rest[0],
716
- sessionContext: rest.length > 1 ? rest.slice(1).join(" ") : undefined,
717
- }
718
- : undefined,
719
- };
720
- case "explain":
721
- return {
722
- ok: true,
723
- command,
724
- input: rest.length > 0 ? { subject: rest.join(" ") } : undefined,
725
- };
726
- case "fallback":
727
- return {
728
- ok: true,
729
- command,
730
- input: rest.length > 0 ? { ref: rest.join(" ") } : undefined,
731
- };
732
- case "storage_smoke": {
733
- const wantRepair = rest[0] === "repair" || rest.includes("--repair");
734
- return {
735
- ok: true,
736
- command,
737
- input: wantRepair ? { runRepairFixture: true } : undefined,
738
- };
739
- }
740
- default:
741
- return {
742
- ok: true,
743
- command,
744
- input: undefined,
745
- };
746
- }
747
- }
748
- function createRuntimeService() {
749
- return {
750
- id: "second-nature-runtime",
751
- start() {
752
- const spine = ensureActivationSpine();
753
- recordRuntimeEvidence(spine, "service_start");
754
- return {
755
- ready: spine.runtimeHandle.ready,
756
- version: spine.runtimeHandle.version,
757
- };
758
- },
759
- };
760
- }
761
- function createLifecycleService() {
762
- return {
763
- id: "second-nature-lifecycle",
764
- start() {
765
- const spine = ensureActivationSpine();
766
- return {
767
- phase: spine.lifecycleState.phase,
768
- registerCount: spine.lifecycleState.registerCount,
769
- lastChangedAt: spine.lifecycleState.lastChangedAt,
770
- };
771
- },
772
- };
773
- }
774
- export default definePluginEntry({
775
- id: "second-nature",
776
- name: "Second Nature",
777
- description: "Registers command/tool/service surface with load-reload lifecycle semantics.",
778
- register(api) {
779
- process.stderr.write(`[second-nature] register() entered, api keys=${Object.keys(api).join(",")}\n`);
780
- const runtimeService = createRuntimeService();
781
- const lifecycleService = createLifecycleService();
782
- api.registerService(runtimeService);
783
- api.registerService(lifecycleService);
784
- api.registerCommand({
785
- name: "second-nature",
786
- description: "Route Agent-facing operational commands for Second Nature.",
787
- acceptsArgs: true,
788
- handler: async (ctx) => {
789
- const spine = ensureActivationSpine();
790
- const parsed = parseCommandInput(ctx.args);
791
- if (!parsed.ok) {
792
- return {
793
- text: JSON.stringify(parsed.result),
794
- };
795
- }
796
- const resolved = spine.router.resolve(parsed.command);
797
- if (!resolved) {
798
- return {
799
- text: JSON.stringify({ ok: false, command: parsed.command, message: "Unknown Second Nature command." }),
800
- };
801
- }
802
- const result = await routeSecondNatureCommand(spine, parsed.command, parsed.input);
803
- return {
804
- text: JSON.stringify(result),
805
- };
806
- },
807
- });
808
- api.registerTool({
809
- name: "second_nature_ops",
810
- description: "Access the Second Nature command surface through a single tool shell.",
811
- parameters: {
812
- type: "object",
813
- additionalProperties: false,
814
- properties: {
815
- command: { type: "string" },
816
- args: { type: "object", additionalProperties: true },
817
- workspaceRoot: {
818
- type: "string",
819
- description: "Workspace root for packaged smoke/runtime alignment (optional; prefer SECOND_NATURE_WORKSPACE_ROOT).",
820
- },
821
- },
822
- required: ["command"],
823
- },
824
- async execute(_id, params) {
825
- const spine = ensureActivationSpine();
826
- syncWorkspaceRootFromTool(spine, params.workspaceRoot);
827
- const resolved = spine.router.resolve(params.command);
828
- if (!resolved) {
829
- return {
830
- content: [
831
- {
832
- type: "text",
833
- text: JSON.stringify({ ok: false, message: "Unknown Second Nature command." }),
834
- },
835
- ],
836
- };
837
- }
838
- const result = await routeSecondNatureCommand(spine, params.command, params.args);
839
- return {
840
- content: [
841
- {
842
- type: "text",
843
- text: JSON.stringify(result),
844
- },
845
- ],
846
- };
847
- },
848
- });
849
- process.stderr.write("[second-nature] register() completed\n");
850
- },
851
- });
1
+ /**
2
+ * Host-safe Second Nature plugin surface.
3
+ *
4
+ * Core logic:
5
+ * - keep register(api) synchronous so OpenClaw captures services/command/tool before return
6
+ * - avoid importing CLI/runtime DB modules at module-evaluation time because the packaged
7
+ * runtime graph currently contains async sql.js bootstrap that breaks vm sandbox loading
8
+ * - expose a minimal in-memory activation spine so status/lifecycle stay truthful even when
9
+ * the full workspace runtime is not loaded inside the host
10
+ *
11
+ * Dependencies:
12
+ * - only imports runtime lifecycle/service modules that are synchronous at load time
13
+ *
14
+ * Boundaries:
15
+ * - read-only operator flows stay available through command/tool surface
16
+ * - structured mutating flows such as policy set / credential verify remain unavailable here
17
+ * - full evidence-backed workspace runtime can be reintroduced later behind a host-safe boundary
18
+ *
19
+ * Plugin classification (verified against OpenClaw 2026.5.4 internals, see
20
+ * docs/validation/openclaw-plugin-classification.md and the explore reports
21
+ * dated 2026-05-06):
22
+ * - Second Nature is a TOOL plugin (exposes `second_nature_ops` to agent
23
+ * sessions). It is intentionally NOT a channel/provider/context-engine.
24
+ * - OpenClaw's `loadGatewayStartupPluginPlan` only loads plugins that opt in
25
+ * via `manifest.activation.onStartup === true`, occupy a configured slot
26
+ * (channel/contextEngine/provider), or declare an explicit hook intent. A
27
+ * tool-only plugin without `activation.onStartup` will be enabled in the
28
+ * registry yet never loaded by the gateway daemon — register(api) only fires
29
+ * inside the `openclaw plugins enable` CLI process, which produces the
30
+ * illusion of a working plugin while agent sessions see no tool. We hit
31
+ * exactly that on 2026-05-06; the fix lives in plugin/openclaw.plugin.json
32
+ * under the `activation` block.
33
+ * - `Shape: non-capability` reported by `openclaw plugins info` is EXPECTED
34
+ * for this plugin. OpenClaw counts capabilities only across cli-backend /
35
+ * text-inference / speech / realtime-* / media-understanding /
36
+ * image-generation / web-search / agent-harness / context-engine / channel.
37
+ * Tool/command/service contributions never bump that count. Pretending to
38
+ * be a context engine with a stub factory just to flip the label would lie
39
+ * to the host (ContextEngine.ingest/assemble/compact get called for real).
40
+ * When Second Nature ships a genuine context-engine layer in a future
41
+ * release, the shape will move to plain-capability honestly.
42
+ *
43
+ * OpenClaw operator norm (T1.1.4 / T1.1.5): set `SECOND_NATURE_WORKSPACE_ROOT` or tool `workspaceRoot` to the
44
+ * **same absolute path** as the OpenClaw **agent workspace** (default `~/.openclaw/workspace`, or
45
+ * `agents.defaults.workspace` in `~/.openclaw/openclaw.json`). Do **not** infer that root from the plugin
46
+ * install directory. With **sandbox** or **per-agent workspaces**, use the path where `data/state.db` and
47
+ * `workspace/` anchors actually live. See `explore/reports/2026-05-04_openclaw-plugin-install-vs-workspace-root.md`.
48
+ *
49
+ * Test coverage:
50
+ * - tests/integration/cli/plugin-runtime-registration.test.ts
51
+ * - tests/integration/cli/plugin-packaging-walkthrough.test.ts
52
+ * - tests/integration/cli/plugin-workspace-ops-bridge.test.ts (T1.1.4 / CH-13 matrix, T1.1.5 ops docs cross-ref)
53
+ */
54
+ import { startRuntimeService, } from "./runtime/core/second-nature/runtime/service-entry.js";
55
+ import { getLifecycleState, recordRegistration, } from "./runtime/core/second-nature/runtime/lifecycle-service.js";
56
+ import { openWorkspaceOpsBridge } from "./workspace-ops-bridge.js";
57
+ // definePluginEntry is OpenClaw's canonical factory for non-channel plugins
58
+ // (provider/tool/command/service/memory/context-engine). At runtime it returns
59
+ // a plain options object; it does NOT add a brand symbol — earlier debugging
60
+ // rounds wrongly assumed the factory was the "plain-capability" marker. The
61
+ // real classification happens via manifest fields (see file header). We still
62
+ // use the factory because it is the documented, supported entry shape, and
63
+ // keeping it future-proof against SDK option-processing changes.
64
+ //
65
+ // IMPORTANT — keep this a STATIC import. The packaged runtime is loaded inside
66
+ // OpenClaw's vm sandbox, which rejects top-level await (manifests as
67
+ // "SyntaxError: Unexpected identifier 'Promise'" at host load time). The same
68
+ // constraint applies to the sql.js async bootstrap noted in the file header.
69
+ // In production the host always provides `openclaw` as a sibling module under
70
+ // ~/.openclaw/npm/node_modules/, so this resolves synchronously. Locally,
71
+ // `openclaw` is declared as a devDependency so build and tests resolve via
72
+ // the same import path.
73
+ import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
74
+ // Stderr sentinels make daemon load-path observable in `gateway.log`. Three
75
+ // lines should appear at startup: "module evaluated", "register() entered ...",
76
+ // "register() completed". Their absence after `openclaw gateway run` proves
77
+ // the daemon never reached this entry — typically a manifest activation gap.
78
+ process.stderr.write("[second-nature] module evaluated\n");
79
+ const INTERNAL_RUNTIME_TRACE_PREFIX = "sn-runtime-";
80
+ const HOST_SAFE_LIMITATION_MESSAGE = "Host-safe plugin package keeps synchronous register/load semantics, but mutating workspace runtime flows remain unavailable here.";
81
+ let activationSpine = null;
82
+ /** T1.1.4 — lazily opened full read bridge; closed when workspace root / resolution changes. */
83
+ let workspaceOpsBridge = null;
84
+ function disposeWorkspaceOpsBridge() {
85
+ if (workspaceOpsBridge) {
86
+ workspaceOpsBridge.close();
87
+ workspaceOpsBridge = null;
88
+ }
89
+ }
90
+ const WORKSPACE_BRIDGE_COMMANDS = new Set([
91
+ "status",
92
+ "quiet",
93
+ "report",
94
+ "session",
95
+ "explain",
96
+ "heartbeat_check",
97
+ "fallback",
98
+ "storage_smoke",
99
+ ]);
100
+ function isWorkspaceBridgeCommand(command, input) {
101
+ if (command === "credential") {
102
+ const action = typeof input?.action === "string" ? input.action : "show";
103
+ return action !== "verify";
104
+ }
105
+ return WORKSPACE_BRIDGE_COMMANDS.has(command);
106
+ }
107
+ async function ensureWorkspaceOpsBridge(spine) {
108
+ const root = spine.workspaceRootContext.runtimeRoot;
109
+ if (workspaceOpsBridge?.root === root) {
110
+ return { ok: true, dispatch: workspaceOpsBridge.dispatch };
111
+ }
112
+ disposeWorkspaceOpsBridge();
113
+ const opened = await openWorkspaceOpsBridge(root);
114
+ if (!opened.ok) {
115
+ return opened;
116
+ }
117
+ workspaceOpsBridge = { root, close: opened.close, dispatch: opened.dispatch };
118
+ return { ok: true, dispatch: opened.dispatch };
119
+ }
120
+ async function routeSecondNatureCommand(spine, command, input) {
121
+ const wr = spine.workspaceRootContext;
122
+ const useBridge = wr.resolution !== "unknown" && isWorkspaceBridgeCommand(command, input);
123
+ if (useBridge) {
124
+ const bridge = await ensureWorkspaceOpsBridge(spine);
125
+ if (!bridge.ok) {
126
+ return {
127
+ ok: false,
128
+ surfaceMode: "host_safe_carrier",
129
+ workspaceReadModelsEvaluated: false,
130
+ message: HOST_SAFE_LIMITATION_MESSAGE,
131
+ error: bridge.error,
132
+ data: {
133
+ workspaceRootResolution: wr.resolution,
134
+ bridgeAttempted: true,
135
+ declaredRoot: wr.declaredRoot,
136
+ },
137
+ };
138
+ }
139
+ return (await bridge.dispatch(command, input));
140
+ }
141
+ const def = spine.router.resolve(command);
142
+ if (!def) {
143
+ return { ok: false, message: `Unknown Second Nature command: ${command}` };
144
+ }
145
+ return def.execute(input);
146
+ }
147
+ function resolveWorkspaceRoot(toolWorkspaceRoot) {
148
+ const env = process.env.SECOND_NATURE_WORKSPACE_ROOT?.trim();
149
+ if (env) {
150
+ return { resolution: "env", declaredRoot: env, runtimeRoot: env };
151
+ }
152
+ const tool = toolWorkspaceRoot?.trim();
153
+ if (tool) {
154
+ return { resolution: "tool_args", declaredRoot: tool, runtimeRoot: tool };
155
+ }
156
+ return { resolution: "unknown", declaredRoot: undefined, runtimeRoot: process.cwd() };
157
+ }
158
+ function syncWorkspaceRootFromTool(spine, toolWorkspaceRoot) {
159
+ const next = resolveWorkspaceRoot(toolWorkspaceRoot);
160
+ const prev = spine.workspaceRootContext;
161
+ const changed = next.runtimeRoot !== prev.runtimeRoot || next.resolution !== prev.resolution;
162
+ if (changed) {
163
+ disposeWorkspaceOpsBridge();
164
+ }
165
+ spine.workspaceRootContext = next;
166
+ if (changed) {
167
+ spine.runtimeHandle = startRuntimeService({ workspaceRoot: next.runtimeRoot });
168
+ }
169
+ }
170
+ function trimRuntimeEvidence(spine) {
171
+ if (spine.runtimeEvidence.length > 12) {
172
+ spine.runtimeEvidence.splice(0, spine.runtimeEvidence.length - 12);
173
+ }
174
+ }
175
+ function latestRuntimeEvidence(spine) {
176
+ return spine.runtimeEvidence[spine.runtimeEvidence.length - 1];
177
+ }
178
+ function createUnavailableActionError(code, message, requiredUserInput, nextStep) {
179
+ return {
180
+ ok: false,
181
+ error: {
182
+ code,
183
+ message,
184
+ requiredUserInput,
185
+ nextStep,
186
+ },
187
+ message: HOST_SAFE_LIMITATION_MESSAGE,
188
+ };
189
+ }
190
+ function parseExplainSubject(subjectRaw) {
191
+ const trimmed = subjectRaw.trim();
192
+ if (!trimmed) {
193
+ throw new Error("explain_subject_invalid");
194
+ }
195
+ const separatorIndex = trimmed.indexOf(":");
196
+ if (separatorIndex === -1) {
197
+ throw new Error("explain_subject_requires_id");
198
+ }
199
+ const kind = trimmed.slice(0, separatorIndex).trim();
200
+ const id = trimmed.slice(separatorIndex + 1).trim();
201
+ if (!id) {
202
+ throw new Error("explain_subject_requires_id");
203
+ }
204
+ switch (kind) {
205
+ case "decision":
206
+ return { subjectType: "decision", subjectId: id };
207
+ case "platform":
208
+ case "platform-selection":
209
+ return { subjectType: "platform-selection", subjectId: id };
210
+ case "outreach":
211
+ return { subjectType: "outreach", subjectId: id };
212
+ case "soul":
213
+ case "soul-change":
214
+ return { subjectType: "soul-change", subjectId: id };
215
+ case "fallback":
216
+ return { subjectType: "fallback", subjectId: id };
217
+ case "probe":
218
+ return { subjectType: "probe", subjectId: id };
219
+ case "report":
220
+ return { subjectType: "report", subjectId: id };
221
+ case "delivery":
222
+ return { subjectType: "delivery", subjectId: id };
223
+ case "source":
224
+ case "source_ref":
225
+ return { subjectType: "source_ref", subjectId: id };
226
+ default:
227
+ throw new Error("explain_subject_unsupported");
228
+ }
229
+ }
230
+ function buildStatusPayload(spine) {
231
+ const runtimeEvidence = latestRuntimeEvidence(spine);
232
+ const updatedAt = runtimeEvidence?.createdAt ?? new Date(spine.lifecycleState.lastChangedAt).toISOString();
233
+ const wr = spine.workspaceRootContext;
234
+ const needsRootHint = wr.resolution === "unknown";
235
+ return {
236
+ ok: false,
237
+ surfaceMode: "host_safe_carrier",
238
+ workspaceReadModelsEvaluated: false,
239
+ message: HOST_SAFE_LIMITATION_MESSAGE,
240
+ error: {
241
+ code: "WORKSPACE_READ_SURFACE_UNAVAILABLE",
242
+ message: "Aggregated status requires workspace state; the host-safe plugin does not load persisted read models on this surface.",
243
+ requiredUserInput: needsRootHint ? ["SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot"] : [],
244
+ nextStep: "run_workspace_second_nature_cli_or_full_runtime_package",
245
+ },
246
+ data: {
247
+ workspaceRootResolution: wr.resolution,
248
+ carrier: {
249
+ host: "openclaw-plugin",
250
+ serviceStatus: spine.runtimeHandle.ready ? "running" : "idle",
251
+ updatedAt,
252
+ lastRuntimeTraceId: runtimeEvidence?.traceId,
253
+ },
254
+ },
255
+ };
256
+ }
257
+ function buildQuietPayload(spine, scope) {
258
+ const wr = spine.workspaceRootContext;
259
+ return {
260
+ ok: false,
261
+ surfaceMode: "host_safe_carrier",
262
+ workspaceReadModelsEvaluated: false,
263
+ message: HOST_SAFE_LIMITATION_MESSAGE,
264
+ error: {
265
+ code: "QUIET_READ_SURFACE_UNAVAILABLE",
266
+ message: "Quiet read surface requires workspace runtime; not evaluated in host-safe carrier mode.",
267
+ requiredUserInput: wr.resolution === "unknown" ? ["SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot"] : [],
268
+ nextStep: "run_workspace_second_nature_cli_or_full_runtime_package",
269
+ },
270
+ data: {
271
+ scope,
272
+ evaluated: false,
273
+ unavailableReason: "host_safe_carrier_no_workspace_db",
274
+ workspaceRootResolution: wr.resolution,
275
+ },
276
+ };
277
+ }
278
+ function buildReportPayload(spine, day) {
279
+ const wr = spine.workspaceRootContext;
280
+ return {
281
+ ok: false,
282
+ surfaceMode: "host_safe_carrier",
283
+ workspaceReadModelsEvaluated: false,
284
+ message: HOST_SAFE_LIMITATION_MESSAGE,
285
+ error: {
286
+ code: "REPORT_READ_SURFACE_UNAVAILABLE",
287
+ message: "Daily report artifacts require workspace runtime.",
288
+ requiredUserInput: wr.resolution === "unknown" ? ["SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot"] : [],
289
+ nextStep: "run_workspace_second_nature_cli_or_full_runtime_package",
290
+ },
291
+ data: {
292
+ evaluated: false,
293
+ unavailableReason: "host_safe_carrier_no_workspace_db",
294
+ day: day && day.trim() ? day : new Date().toISOString().slice(0, 10),
295
+ workspaceRootResolution: wr.resolution,
296
+ },
297
+ };
298
+ }
299
+ function buildSessionPayload(spine, sessionId) {
300
+ if (!sessionId) {
301
+ return {
302
+ ok: false,
303
+ error: {
304
+ code: "MISSING_SESSION_ID",
305
+ message: "session show requires sessionId",
306
+ requiredUserInput: ["session_id"],
307
+ nextStep: "reinvoke_session_with_session_id",
308
+ },
309
+ };
310
+ }
311
+ const wr = spine.workspaceRootContext;
312
+ return {
313
+ ok: false,
314
+ surfaceMode: "host_safe_carrier",
315
+ workspaceReadModelsEvaluated: false,
316
+ message: HOST_SAFE_LIMITATION_MESSAGE,
317
+ error: {
318
+ code: "SESSION_READ_SURFACE_UNAVAILABLE",
319
+ message: "Session analytics require workspace state database.",
320
+ requiredUserInput: wr.resolution === "unknown" ? ["SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot"] : [],
321
+ nextStep: "run_workspace_second_nature_cli_or_full_runtime_package",
322
+ },
323
+ data: {
324
+ requestedSessionId: sessionId,
325
+ evaluated: false,
326
+ unavailableReason: "host_safe_carrier_no_workspace_db",
327
+ workspaceRootResolution: wr.resolution,
328
+ },
329
+ };
330
+ }
331
+ function buildCredentialPayload(spine, platformId) {
332
+ const wr = spine.workspaceRootContext;
333
+ return {
334
+ ok: false,
335
+ surfaceMode: "host_safe_carrier",
336
+ workspaceReadModelsEvaluated: false,
337
+ message: HOST_SAFE_LIMITATION_MESSAGE,
338
+ error: {
339
+ code: "CREDENTIAL_READ_SURFACE_UNAVAILABLE",
340
+ message: "Credential inspection requires workspace runtime on this surface.",
341
+ requiredUserInput: wr.resolution === "unknown" ? ["SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot"] : [],
342
+ nextStep: "run_workspace_second_nature_cli_or_full_runtime_package",
343
+ },
344
+ data: {
345
+ platformId: platformId && platformId.trim() ? platformId : undefined,
346
+ evaluated: false,
347
+ unavailableReason: "host_safe_carrier_no_workspace_db",
348
+ workspaceRootResolution: wr.resolution,
349
+ },
350
+ };
351
+ }
352
+ function buildExplainPayload(spine, subjectRaw) {
353
+ if (!subjectRaw?.trim()) {
354
+ return {
355
+ ok: false,
356
+ error: {
357
+ code: "MISSING_EXPLAIN_SUBJECT",
358
+ message: "explain requires subject",
359
+ requiredUserInput: ["subject"],
360
+ nextStep: "reinvoke_explain_with_subject",
361
+ },
362
+ };
363
+ }
364
+ let subject;
365
+ try {
366
+ subject = parseExplainSubject(subjectRaw);
367
+ }
368
+ catch (error) {
369
+ const code = error.message;
370
+ if (code === "explain_subject_requires_id") {
371
+ return createUnavailableActionError("EXPLAIN_SUBJECT_REQUIRES_ID", "subject must include identifier", ["subject"], "reinvoke_explain_with_supported_subject");
372
+ }
373
+ if (code === "explain_subject_unsupported") {
374
+ return createUnavailableActionError("EXPLAIN_SUBJECT_UNSUPPORTED", "supported subjects include decision:, platform:, outreach:, soul:, fallback:, delivery:, probe:, report:, source:", ["subject"], "reinvoke_explain_with_supported_subject");
375
+ }
376
+ return createUnavailableActionError("EXPLAIN_SUBJECT_INVALID", "invalid explain subject", ["subject"], "reinvoke_explain_with_supported_subject");
377
+ }
378
+ const wr = spine.workspaceRootContext;
379
+ return {
380
+ ok: false,
381
+ surfaceMode: "host_safe_carrier",
382
+ workspaceReadModelsEvaluated: false,
383
+ message: HOST_SAFE_LIMITATION_MESSAGE,
384
+ error: {
385
+ code: "EXPLAIN_READ_SURFACE_UNAVAILABLE",
386
+ message: "Evidence-backed explain requires persisted workspace read models; host-safe carrier did not evaluate operator explain (CH-11-02).",
387
+ requiredUserInput: wr.resolution === "unknown" ? ["SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot"] : [],
388
+ nextStep: "run_workspace_second_nature_cli_or_full_runtime_package",
389
+ },
390
+ data: {
391
+ subjectType: subject.subjectType,
392
+ evaluated: false,
393
+ workspaceRootResolution: wr.resolution,
394
+ },
395
+ };
396
+ }
397
+ async function buildStorageSmokePayload(input) {
398
+ try {
399
+ const mod = await import("./runtime/storage/bootstrap/storage-mode-smoke.js");
400
+ const runRepairFixture = Boolean(input?.runRepairFixture);
401
+ const workspaceRoot = typeof input?.workspaceRoot === "string" ? input.workspaceRoot : undefined;
402
+ const data = await mod.runStorageModeSmoke({ runRepairFixture, workspaceRoot });
403
+ return { ok: true, data };
404
+ }
405
+ catch (error) {
406
+ return {
407
+ ok: false,
408
+ message: error instanceof Error ? error.message : String(error),
409
+ error: {
410
+ code: "STORAGE_SMOKE_LOAD_FAILED",
411
+ message: "Could not load packaged storage-mode smoke module",
412
+ nextStep: "rebuild_plugin_runtime_package",
413
+ },
414
+ };
415
+ }
416
+ }
417
+ function buildFallbackHostSafePayload(ref) {
418
+ if (!ref?.trim()) {
419
+ return {
420
+ ok: false,
421
+ error: {
422
+ code: "MISSING_FALLBACK_REF",
423
+ message: "fallback requires ref (e.g. fallback:…)",
424
+ requiredUserInput: ["ref"],
425
+ nextStep: "reinvoke_with_ref",
426
+ },
427
+ };
428
+ }
429
+ return createUnavailableActionError("HOST_SAFE_FALLBACK_VIEW_UNAVAILABLE", "Operator fallback view requires workspace state database; host-safe plugin cannot read persisted fallback artifacts.", ["ref"], "run_workspace_second_nature_cli_or_full_runtime_package");
430
+ }
431
+ function isProbeOnlyInput(input) {
432
+ const v = input?.probeOnly;
433
+ return v === true || v === "true" || v === 1 || v === "1";
434
+ }
435
+ function buildHeartbeatCheckPayload(spine, input) {
436
+ const runtimeEvidence = latestRuntimeEvidence(spine);
437
+ const updatedAt = runtimeEvidence?.createdAt ?? new Date(spine.lifecycleState.lastChangedAt).toISOString();
438
+ const timestamp = typeof input?.timestamp === "string" && input.timestamp.trim().length > 0 ? input.timestamp : updatedAt;
439
+ const wr = spine.workspaceRootContext;
440
+ if (isProbeOnlyInput(input)) {
441
+ return {
442
+ ok: true,
443
+ status: "heartbeat_ok",
444
+ surfaceMode: "capability_probe",
445
+ reasons: ["probe_only"],
446
+ livedExperienceLoopClaimed: false,
447
+ scope: "rhythm",
448
+ trigger: "heartbeat_bridge",
449
+ message: "Capability probe only on the host-safe carrier surface; does not claim a full lived-experience decision loop.",
450
+ data: {
451
+ workspaceRootResolution: wr.resolution,
452
+ runtime: {
453
+ host: "openclaw-plugin",
454
+ serviceStatus: spine.runtimeHandle.ready ? "running" : "idle",
455
+ updatedAt,
456
+ },
457
+ surface: {
458
+ tool: "second_nature_ops",
459
+ command: "second-nature heartbeat_check",
460
+ },
461
+ bridge: {
462
+ timestamp,
463
+ probeOnly: true,
464
+ sessionContextProvided: typeof input?.sessionContext === "string" && input.sessionContext.trim().length > 0,
465
+ heartbeatChecklistProvided: typeof input?.heartbeatChecklist === "string" && input.heartbeatChecklist.trim().length > 0,
466
+ serviceEntryMode: "capability_probe",
467
+ },
468
+ },
469
+ };
470
+ }
471
+ return {
472
+ ok: true,
473
+ status: "runtime_carrier_only",
474
+ surfaceMode: "host_safe_carrier",
475
+ livedExperienceLoopClaimed: false,
476
+ scope: "rhythm",
477
+ trigger: "heartbeat_bridge",
478
+ reasons: ["runtime_carrier_only", "host_safe_bridge_ack"],
479
+ nextAction: "continue_carrier_surface_only",
480
+ message: "Packaged carrier acknowledged this heartbeat round. This is not a full lived-experience decision loop; use the workspace CLI when read models are required.",
481
+ data: {
482
+ workspaceRootResolution: wr.resolution,
483
+ runtime: {
484
+ host: "openclaw-plugin",
485
+ serviceStatus: spine.runtimeHandle.ready ? "running" : "idle",
486
+ updatedAt,
487
+ },
488
+ surface: {
489
+ tool: "second_nature_ops",
490
+ command: "second-nature heartbeat_check",
491
+ },
492
+ bridge: {
493
+ timestamp,
494
+ sessionContextProvided: typeof input?.sessionContext === "string" && input.sessionContext.trim().length > 0,
495
+ heartbeatChecklistProvided: typeof input?.heartbeatChecklist === "string" && input.heartbeatChecklist.trim().length > 0,
496
+ serviceEntryMode: "runtime_carrier_only",
497
+ },
498
+ },
499
+ };
500
+ }
501
+ function createHostSafeRouter(spine) {
502
+ const notImplemented = async (command) => ({
503
+ ok: false,
504
+ command,
505
+ message: HOST_SAFE_LIMITATION_MESSAGE,
506
+ });
507
+ const commands = [
508
+ {
509
+ name: "status",
510
+ description: "Show aggregated Second Nature status",
511
+ execute: async () => buildStatusPayload(spine),
512
+ },
513
+ {
514
+ name: "policy",
515
+ description: "Write or inspect policy state",
516
+ execute: async (input) => {
517
+ const action = typeof input?.action === "string" ? input.action : "show";
518
+ if (action === "set") {
519
+ return createUnavailableActionError("HOST_SAFE_POLICY_SET_UNAVAILABLE", "policy set is unavailable in the host-safe plugin package", ["social_daily_limit", "quiet_enabled"], "run_workspace_runtime_or_reinstall_full_build");
520
+ }
521
+ return notImplemented("policy");
522
+ },
523
+ },
524
+ {
525
+ name: "credential",
526
+ description: "Inspect or recover credential state",
527
+ execute: async (input) => {
528
+ const action = typeof input?.action === "string" ? input.action : "show";
529
+ if (action === "verify") {
530
+ return createUnavailableActionError("HOST_SAFE_CREDENTIAL_VERIFY_UNAVAILABLE", "credential verify is unavailable in the host-safe plugin package", ["verification_answer"], "run_workspace_runtime_or_reinstall_full_build");
531
+ }
532
+ const platformId = typeof input?.platformId === "string" ? input.platformId : undefined;
533
+ return buildCredentialPayload(spine, platformId);
534
+ },
535
+ },
536
+ {
537
+ name: "quiet",
538
+ description: "Inspect Quiet lifecycle state",
539
+ execute: async (input) => {
540
+ const scope = typeof input?.scope === "string" ? input.scope : undefined;
541
+ return buildQuietPayload(spine, scope);
542
+ },
543
+ },
544
+ {
545
+ name: "report",
546
+ description: "Show daily report artifacts",
547
+ execute: async (input) => {
548
+ const day = typeof input?.day === "string" ? input.day : undefined;
549
+ return buildReportPayload(spine, day);
550
+ },
551
+ },
552
+ {
553
+ name: "session",
554
+ description: "Inspect continuity session details",
555
+ execute: async (input) => {
556
+ const sessionId = typeof input?.sessionId === "string" ? input.sessionId : undefined;
557
+ return buildSessionPayload(spine, sessionId);
558
+ },
559
+ },
560
+ {
561
+ name: "audit",
562
+ description: "Inspect audit and evidence views",
563
+ execute: async () => notImplemented("audit"),
564
+ },
565
+ {
566
+ name: "explain",
567
+ description: "Answer why-question explain requests",
568
+ execute: async (input) => {
569
+ const subject = typeof input?.subject === "string" ? input.subject : undefined;
570
+ return buildExplainPayload(spine, subject);
571
+ },
572
+ },
573
+ {
574
+ name: "heartbeat_check",
575
+ description: "Acknowledge the shipping heartbeat bridge round",
576
+ execute: async (input) => buildHeartbeatCheckPayload(spine, input),
577
+ },
578
+ {
579
+ name: "fallback",
580
+ description: "Operator-visible delivery fallback view (full workspace runtime required)",
581
+ execute: async (input) => {
582
+ const ref = typeof input?.ref === "string" ? input.ref.trim() : undefined;
583
+ return buildFallbackHostSafePayload(ref);
584
+ },
585
+ },
586
+ {
587
+ name: "storage_smoke",
588
+ description: "T4.1.4 storage mode smoke report (sql.js vs native probe)",
589
+ execute: async (input) => buildStorageSmokePayload(input),
590
+ },
591
+ ];
592
+ return {
593
+ commands,
594
+ resolve(name) {
595
+ return commands.find((command) => command.name === name);
596
+ },
597
+ };
598
+ }
599
+ function createActivationSpine() {
600
+ const workspaceRootContext = resolveWorkspaceRoot(undefined);
601
+ const spine = {
602
+ router: undefined,
603
+ runtimeHandle: startRuntimeService({ workspaceRoot: workspaceRootContext.runtimeRoot }),
604
+ lifecycleState: getLifecycleState(),
605
+ serviceStartRecorded: false,
606
+ runtimeEvidence: [],
607
+ workspaceRootContext,
608
+ };
609
+ spine.router = createHostSafeRouter(spine);
610
+ return spine;
611
+ }
612
+ function ensureActivationSpine() {
613
+ if (activationSpine) {
614
+ return activationSpine;
615
+ }
616
+ activationSpine = createActivationSpine();
617
+ return activationSpine;
618
+ }
619
+ function recordRuntimeEvidence(spine, origin) {
620
+ if (origin === "service_start" && spine.serviceStartRecorded) {
621
+ return;
622
+ }
623
+ if (origin === "service_start") {
624
+ spine.serviceStartRecorded = true;
625
+ }
626
+ spine.runtimeEvidence.push({
627
+ traceId: `${INTERNAL_RUNTIME_TRACE_PREFIX}${origin}-${spine.lifecycleState.registerCount}-${Date.now()}`,
628
+ capability: origin === "register"
629
+ ? spine.lifecycleState.registerCount === 1
630
+ ? "runtime.activate"
631
+ : "runtime.reload"
632
+ : "runtime.heartbeat",
633
+ origin,
634
+ createdAt: new Date().toISOString(),
635
+ status: "succeeded",
636
+ });
637
+ trimRuntimeEvidence(spine);
638
+ }
639
+ function refreshRegistrationState() {
640
+ const spine = ensureActivationSpine();
641
+ const workspaceRootContext = resolveWorkspaceRoot(undefined);
642
+ const prev = spine.workspaceRootContext;
643
+ const changed = workspaceRootContext.runtimeRoot !== prev.runtimeRoot || workspaceRootContext.resolution !== prev.resolution;
644
+ if (changed) {
645
+ disposeWorkspaceOpsBridge();
646
+ }
647
+ spine.workspaceRootContext = workspaceRootContext;
648
+ spine.runtimeHandle = startRuntimeService({ workspaceRoot: workspaceRootContext.runtimeRoot });
649
+ spine.lifecycleState = recordRegistration();
650
+ spine.serviceStartRecorded = false;
651
+ recordRuntimeEvidence(spine, "register");
652
+ return spine;
653
+ }
654
+ function parseCommandInput(rawArgs) {
655
+ const tokens = rawArgs?.trim().split(/\s+/).filter(Boolean) ?? [];
656
+ if (tokens.length === 0) {
657
+ return {
658
+ ok: false,
659
+ result: { ok: false, message: "Missing command argument." },
660
+ };
661
+ }
662
+ const [command, ...rest] = tokens;
663
+ if (command === "policy" && rest[0] === "set") {
664
+ return {
665
+ ok: false,
666
+ result: {
667
+ ok: false,
668
+ command,
669
+ message: "policy set requires structured args; use second_nature_ops instead.",
670
+ },
671
+ };
672
+ }
673
+ if (command === "credential" && rest[0] === "verify") {
674
+ return {
675
+ ok: false,
676
+ result: {
677
+ ok: false,
678
+ command,
679
+ message: "credential verify requires structured args; use second_nature_ops instead.",
680
+ },
681
+ };
682
+ }
683
+ switch (command) {
684
+ case "status":
685
+ case "quiet":
686
+ return {
687
+ ok: true,
688
+ command,
689
+ input: rest.length > 0 ? { scope: rest.join(" ") } : undefined,
690
+ };
691
+ case "report":
692
+ return {
693
+ ok: true,
694
+ command,
695
+ input: rest[0] ? { day: rest[0] } : undefined,
696
+ };
697
+ case "session":
698
+ return {
699
+ ok: true,
700
+ command,
701
+ input: rest[0] ? { sessionId: rest[0] } : undefined,
702
+ };
703
+ case "credential":
704
+ return {
705
+ ok: true,
706
+ command,
707
+ input: rest[0] ? { platformId: rest[0] } : undefined,
708
+ };
709
+ case "heartbeat_check":
710
+ return {
711
+ ok: true,
712
+ command,
713
+ input: rest.length > 0
714
+ ? {
715
+ timestamp: rest[0],
716
+ sessionContext: rest.length > 1 ? rest.slice(1).join(" ") : undefined,
717
+ }
718
+ : undefined,
719
+ };
720
+ case "explain":
721
+ return {
722
+ ok: true,
723
+ command,
724
+ input: rest.length > 0 ? { subject: rest.join(" ") } : undefined,
725
+ };
726
+ case "fallback":
727
+ return {
728
+ ok: true,
729
+ command,
730
+ input: rest.length > 0 ? { ref: rest.join(" ") } : undefined,
731
+ };
732
+ case "storage_smoke": {
733
+ const wantRepair = rest[0] === "repair" || rest.includes("--repair");
734
+ return {
735
+ ok: true,
736
+ command,
737
+ input: wantRepair ? { runRepairFixture: true } : undefined,
738
+ };
739
+ }
740
+ default:
741
+ return {
742
+ ok: true,
743
+ command,
744
+ input: undefined,
745
+ };
746
+ }
747
+ }
748
+ function createRuntimeService() {
749
+ return {
750
+ id: "second-nature-runtime",
751
+ start() {
752
+ const spine = ensureActivationSpine();
753
+ recordRuntimeEvidence(spine, "service_start");
754
+ return {
755
+ ready: spine.runtimeHandle.ready,
756
+ version: spine.runtimeHandle.version,
757
+ };
758
+ },
759
+ };
760
+ }
761
+ function createLifecycleService() {
762
+ return {
763
+ id: "second-nature-lifecycle",
764
+ start() {
765
+ const spine = ensureActivationSpine();
766
+ return {
767
+ phase: spine.lifecycleState.phase,
768
+ registerCount: spine.lifecycleState.registerCount,
769
+ lastChangedAt: spine.lifecycleState.lastChangedAt,
770
+ };
771
+ },
772
+ };
773
+ }
774
+ const SECOND_NATURE_TOOL_SCHEMA = {
775
+ type: "object",
776
+ additionalProperties: false,
777
+ properties: {
778
+ command: { type: "string" },
779
+ args: { type: "object", additionalProperties: true },
780
+ workspaceRoot: {
781
+ type: "string",
782
+ description: "Workspace root for packaged smoke/runtime alignment (optional; prefer SECOND_NATURE_WORKSPACE_ROOT).",
783
+ },
784
+ },
785
+ required: ["command"],
786
+ };
787
+ export default definePluginEntry({
788
+ id: "second-nature",
789
+ name: "Second Nature",
790
+ description: "Registers command/tool/service surface with load-reload lifecycle semantics.",
791
+ register(api) {
792
+ process.stderr.write(`[second-nature] register() entered, api keys=${Object.keys(api).join(",")}\n`);
793
+ const runtimeService = createRuntimeService();
794
+ const lifecycleService = createLifecycleService();
795
+ api.registerService(runtimeService);
796
+ api.registerService(lifecycleService);
797
+ api.registerCommand({
798
+ name: "second-nature",
799
+ description: "Route Agent-facing operational commands for Second Nature.",
800
+ acceptsArgs: true,
801
+ handler: async (ctx) => {
802
+ const spine = ensureActivationSpine();
803
+ const parsed = parseCommandInput(ctx.args);
804
+ if (!parsed.ok) {
805
+ return {
806
+ text: JSON.stringify(parsed.result),
807
+ };
808
+ }
809
+ const resolved = spine.router.resolve(parsed.command);
810
+ if (!resolved) {
811
+ return {
812
+ text: JSON.stringify({ ok: false, command: parsed.command, message: "Unknown Second Nature command." }),
813
+ };
814
+ }
815
+ const result = await routeSecondNatureCommand(spine, parsed.command, parsed.input);
816
+ return {
817
+ text: JSON.stringify(result),
818
+ };
819
+ },
820
+ });
821
+ const executeSecondNatureTool = async (params) => {
822
+ const spine = ensureActivationSpine();
823
+ syncWorkspaceRootFromTool(spine, params.workspaceRoot);
824
+ const resolved = spine.router.resolve(params.command);
825
+ if (!resolved) {
826
+ return {
827
+ content: [
828
+ {
829
+ type: "text",
830
+ text: JSON.stringify({ ok: false, message: "Unknown Second Nature command." }),
831
+ },
832
+ ],
833
+ };
834
+ }
835
+ const result = await routeSecondNatureCommand(spine, params.command, params.args);
836
+ return {
837
+ content: [
838
+ {
839
+ type: "text",
840
+ text: JSON.stringify(result),
841
+ },
842
+ ],
843
+ };
844
+ };
845
+ api.registerTool({
846
+ name: "second_nature_ops",
847
+ description: "Access the Second Nature command surface through a single tool shell.",
848
+ parameters: SECOND_NATURE_TOOL_SCHEMA,
849
+ async execute(_id, params) {
850
+ return executeSecondNatureTool(params);
851
+ },
852
+ });
853
+ process.stderr.write("[second-nature] register() completed\n");
854
+ },
855
+ });