@haaaiawd/second-nature 0.1.38 → 0.1.39

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 (46) hide show
  1. package/agent-inner-guide.md +18 -0
  2. package/index.js +1270 -1262
  3. package/openclaw.plugin.json +29 -29
  4. package/package.json +55 -55
  5. package/runtime/cli/ops/heartbeat-surface.d.ts +75 -60
  6. package/runtime/cli/ops/heartbeat-surface.js +97 -83
  7. package/runtime/cli/ops/ops-router.js +1428 -1282
  8. package/runtime/cli/ops/workspace-heartbeat-runner.js +236 -191
  9. package/runtime/core/second-nature/guidance/apply-guidance.d.ts +12 -10
  10. package/runtime/core/second-nature/guidance/apply-guidance.js +15 -10
  11. package/runtime/core/second-nature/guidance/user-reply-continuity.d.ts +50 -50
  12. package/runtime/core/second-nature/guidance/user-reply-continuity.js +89 -80
  13. package/runtime/core/second-nature/runtime/service-entry.d.ts +39 -36
  14. package/runtime/core/second-nature/runtime/service-entry.js +44 -45
  15. package/runtime/dream/dream-engine.d.ts +14 -0
  16. package/runtime/dream/dream-engine.js +306 -0
  17. package/runtime/dream/dream-input-loader.d.ts +37 -0
  18. package/runtime/dream/dream-input-loader.js +155 -0
  19. package/runtime/dream/dream-scheduler.d.ts +75 -0
  20. package/runtime/dream/dream-scheduler.js +131 -0
  21. package/runtime/dream/index.d.ts +16 -0
  22. package/runtime/dream/index.js +14 -0
  23. package/runtime/dream/insight-extractor.d.ts +32 -0
  24. package/runtime/dream/insight-extractor.js +135 -0
  25. package/runtime/dream/memory-consolidator.d.ts +45 -0
  26. package/runtime/dream/memory-consolidator.js +140 -0
  27. package/runtime/dream/narrative-update-proposal.d.ts +34 -0
  28. package/runtime/dream/narrative-update-proposal.js +83 -0
  29. package/runtime/dream/output-validator.d.ts +20 -0
  30. package/runtime/dream/output-validator.js +110 -0
  31. package/runtime/dream/redaction-gate.d.ts +31 -0
  32. package/runtime/dream/redaction-gate.js +109 -0
  33. package/runtime/dream/relationship-update-proposal.d.ts +27 -0
  34. package/runtime/dream/relationship-update-proposal.js +119 -0
  35. package/runtime/dream/sampler.d.ts +30 -0
  36. package/runtime/dream/sampler.js +65 -0
  37. package/runtime/dream/types.d.ts +187 -0
  38. package/runtime/dream/types.js +11 -0
  39. package/runtime/guidance/fallback.js +20 -17
  40. package/runtime/guidance/guidance-assembler.js +76 -74
  41. package/runtime/guidance/output-guard.d.ts +13 -10
  42. package/runtime/guidance/output-guard.js +53 -29
  43. package/runtime/guidance/template-registry.d.ts +20 -16
  44. package/runtime/guidance/template-registry.js +123 -82
  45. package/runtime/guidance/types.d.ts +98 -84
  46. package/runtime/observability/projections/guidance-audit.js +38 -35
package/index.js CHANGED
@@ -1,1262 +1,1270 @@
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
- * - T4.2.1: owner reply ingestion → RelationshipMemory feedback (full runtime only)
11
- *
12
- * Dependencies:
13
- * - only imports runtime lifecycle/service modules that are synchronous at load time
14
- *
15
- * Boundaries:
16
- * - read-only operator flows stay available through command/tool surface
17
- * - structured mutating flows such as policy set / credential verify remain unavailable here
18
- * - full evidence-backed workspace runtime can be reintroduced later behind a host-safe boundary
19
- *
20
- * Plugin classification (verified against OpenClaw 2026.5.4 internals, see
21
- * docs/validation/openclaw-plugin-classification.md and the explore reports
22
- * dated 2026-05-06):
23
- * - Second Nature is a TOOL plugin (exposes `second_nature_ops` to agent
24
- * sessions). It is intentionally NOT a channel/provider/context-engine.
25
- * - OpenClaw's `loadGatewayStartupPluginPlan` only loads plugins that opt in
26
- * via `manifest.activation.onStartup === true`, occupy a configured slot
27
- * (channel/contextEngine/provider), or declare an explicit hook intent. A
28
- * tool-only plugin without `activation.onStartup` will be enabled in the
29
- * registry yet never loaded by the gateway daemon — register(api) only fires
30
- * inside the `openclaw plugins enable` CLI process, which produces the
31
- * illusion of a working plugin while agent sessions see no tool. We hit
32
- * exactly that on 2026-05-06; the fix lives in plugin/openclaw.plugin.json
33
- * under the `activation` block.
34
- * - `Shape: non-capability` reported by `openclaw plugins info` is EXPECTED
35
- * for this plugin. OpenClaw counts capabilities only across cli-backend /
36
- * text-inference / speech / realtime-* / media-understanding /
37
- * image-generation / web-search / agent-harness / context-engine / channel.
38
- * Tool/command/service contributions never bump that count. Pretending to
39
- * be a context engine with a stub factory just to flip the label would lie
40
- * to the host (ContextEngine.ingest/assemble/compact get called for real).
41
- * When Second Nature ships a genuine context-engine layer in a future
42
- * release, the shape will move to plain-capability honestly.
43
- *
44
- * OpenClaw operator norm (T1.1.4 / T1.1.5): set `SECOND_NATURE_WORKSPACE_ROOT` or tool `workspaceRoot` to the
45
- * **same absolute path** as the OpenClaw **agent workspace** (default `~/.openclaw/workspace`, or
46
- * `agents.defaults.workspace` in `~/.openclaw/openclaw.json`). Do **not** infer that root from the plugin
47
- * 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`.
49
- *
50
- * Test coverage:
51
- * - tests/integration/cli/plugin-runtime-registration.test.ts
52
- * - tests/integration/cli/plugin-packaging-walkthrough.test.ts
53
- * - tests/integration/cli/plugin-workspace-ops-bridge.test.ts (T1.1.4 / CH-13 matrix, T1.1.5 ops docs cross-ref)
54
- */
55
- import fs from "node:fs";
56
- import path from "node:path";
57
- import { fileURLToPath } from "node:url";
58
- import { startRuntimeService, } from "./runtime/core/second-nature/runtime/service-entry.js";
59
- import { getLifecycleState, recordRegistration, } from "./runtime/core/second-nature/runtime/lifecycle-service.js";
60
- import { openWorkspaceOpsBridge } from "./workspace-ops-bridge.js";
61
- // Keep the entry as a plain object instead of importing OpenClaw's
62
- // SDK entry helper. Upload/package validators may import this module
63
- // before the host SDK is installed; a static SDK import turns a valid package
64
- // into ERR_MODULE_NOT_FOUND. OpenClaw classifies this plugin from the manifest
65
- // fields, and the helper returns this same object shape at runtime.
66
- // Stderr sentinels make daemon load-path observable in `gateway.log`. Three
67
- // lines should appear at startup: "module evaluated", "register() entered ...",
68
- // "register() completed". Their absence after `openclaw gateway run` proves
69
- // the daemon never reached this entry typically a manifest activation gap.
70
- process.stderr.write("[second-nature] module evaluated\n");
71
- const INTERNAL_RUNTIME_TRACE_PREFIX = "sn-runtime-";
72
- const HOST_SAFE_LIMITATION_MESSAGE = "Host-safe plugin package keeps synchronous register/load semantics, but mutating workspace runtime flows remain unavailable here.";
73
- const SETUP_MARKER_RELATIVE_PATH = path.join(".second-nature", "setup", "agent-inner-guide-ack.json");
74
- const SETUP_GUIDE_VERSION = "0.1.38";
75
- const SETUP_COMMANDS = new Set(["setup_hint", "setup_ack"]);
76
- let activationSpine = null;
77
- /** T1.1.4 lazily opened full read bridge; closed when workspace root / resolution changes. */
78
- let workspaceOpsBridge = null;
79
- function disposeWorkspaceOpsBridge() {
80
- if (workspaceOpsBridge) {
81
- workspaceOpsBridge.close();
82
- workspaceOpsBridge = null;
83
- }
84
- }
85
- const WORKSPACE_BRIDGE_COMMANDS = new Set([
86
- "status",
87
- "quiet",
88
- "report",
89
- "session",
90
- "explain",
91
- "heartbeat_check",
92
- "fallback",
93
- "storage_smoke",
94
- // T1.2.8 (SN-CODE-03): capability probe surface via workspace bridge
95
- "capability_probe",
96
- // T1.2.6 / T1.2.7: policy show + audit read surface
97
- "policy",
98
- "audit",
99
- // T3.3.2: near-real connector smoke sentinel
100
- "near_real_smoke",
101
- // v6 ops surface (CR8-01): narrative, goal, dream:recent, connector_status/test, cycle:recent
102
- "narrative",
103
- "goal",
104
- "dream:recent",
105
- "connector_status",
106
- "connector_test",
107
- "connector_behavior_add",
108
- "cycle:recent",
109
- // v7 ops surface (T-ROS.C.1 / T-ROS.C.2 / T-ROS.C.3): self_health, tool_affordance, heartbeat_digest,
110
- // narrative:diff, timeline, restore, runtime_secret_bootstrap, connector:run
111
- "self_health",
112
- "tool_affordance",
113
- "heartbeat_digest",
114
- "snapshot:capture",
115
- "narrative:diff",
116
- "timeline",
117
- "restore",
118
- "runtime_secret_bootstrap",
119
- "connector:run",
120
- ]);
121
- function isWorkspaceBridgeCommand(command, input) {
122
- if (command === "credential") {
123
- const action = typeof input?.action === "string" ? input.action : "show";
124
- return action !== "verify";
125
- }
126
- return WORKSPACE_BRIDGE_COMMANDS.has(command);
127
- }
128
- async function ensureWorkspaceOpsBridge(spine) {
129
- const root = spine.workspaceRootContext.runtimeRoot;
130
- if (workspaceOpsBridge?.root === root) {
131
- return { ok: true, dispatch: workspaceOpsBridge.dispatch };
132
- }
133
- disposeWorkspaceOpsBridge();
134
- const opened = await openWorkspaceOpsBridge(root);
135
- if (!opened.ok) {
136
- return opened;
137
- }
138
- workspaceOpsBridge = { root, close: opened.close, dispatch: opened.dispatch };
139
- return { ok: true, dispatch: opened.dispatch };
140
- }
141
- async function routeSecondNatureCommand(spine, command, input) {
142
- const wr = spine.workspaceRootContext;
143
- const useBridge = wr.resolution !== "unknown" && isWorkspaceBridgeCommand(command, input);
144
- if (useBridge) {
145
- const bridge = await ensureWorkspaceOpsBridge(spine);
146
- if (!bridge.ok) {
147
- return {
148
- ok: false,
149
- surfaceMode: "host_safe_carrier",
150
- workspaceReadModelsEvaluated: false,
151
- message: HOST_SAFE_LIMITATION_MESSAGE,
152
- error: bridge.error,
153
- data: {
154
- workspaceRootResolution: wr.resolution,
155
- bridgeAttempted: true,
156
- declaredRoot: wr.declaredRoot,
157
- },
158
- };
159
- }
160
- const payload = (await bridge.dispatch(command, input));
161
- return withSetupNudge(spine, command, payload);
162
- }
163
- const def = spine.router.resolve(command);
164
- if (!def) {
165
- return { ok: false, message: `Unknown Second Nature command: ${command}` };
166
- }
167
- return withSetupNudge(spine, command, await def.execute(input));
168
- }
169
- function resolveWorkspaceRoot(toolWorkspaceRoot) {
170
- const env = process.env.SECOND_NATURE_WORKSPACE_ROOT?.trim();
171
- if (env) {
172
- return { resolution: "env", declaredRoot: env, runtimeRoot: env };
173
- }
174
- const tool = toolWorkspaceRoot?.trim();
175
- if (tool) {
176
- return { resolution: "tool_args", declaredRoot: tool, runtimeRoot: tool };
177
- }
178
- return {
179
- resolution: "unknown",
180
- declaredRoot: undefined,
181
- runtimeRoot: process.cwd(),
182
- };
183
- }
184
- function syncWorkspaceRootFromTool(spine, toolWorkspaceRoot) {
185
- const next = resolveWorkspaceRoot(toolWorkspaceRoot);
186
- const prev = spine.workspaceRootContext;
187
- const changed = next.runtimeRoot !== prev.runtimeRoot ||
188
- next.resolution !== prev.resolution;
189
- if (changed) {
190
- disposeWorkspaceOpsBridge();
191
- }
192
- spine.workspaceRootContext = next;
193
- if (changed) {
194
- spine.runtimeHandle = startRuntimeService({
195
- workspaceRoot: next.runtimeRoot,
196
- });
197
- }
198
- }
199
- function trimRuntimeEvidence(spine) {
200
- if (spine.runtimeEvidence.length > 12) {
201
- spine.runtimeEvidence.splice(0, spine.runtimeEvidence.length - 12);
202
- }
203
- }
204
- function latestRuntimeEvidence(spine) {
205
- return spine.runtimeEvidence[spine.runtimeEvidence.length - 1];
206
- }
207
- function createUnavailableActionError(code, message, requiredUserInput, nextStep) {
208
- return {
209
- ok: false,
210
- error: {
211
- code,
212
- message,
213
- requiredUserInput,
214
- nextStep,
215
- },
216
- message: HOST_SAFE_LIMITATION_MESSAGE,
217
- };
218
- }
219
- function getPluginPackageRoot() {
220
- return path.dirname(fileURLToPath(import.meta.url));
221
- }
222
- function safeShortText(value, maxLength = 240) {
223
- if (typeof value !== "string") {
224
- return undefined;
225
- }
226
- const trimmed = value.trim();
227
- if (!trimmed) {
228
- return undefined;
229
- }
230
- return trimmed.length > maxLength
231
- ? `${trimmed.slice(0, maxLength - 3)}...`
232
- : trimmed;
233
- }
234
- function resolveSetupMarkerPath(spine) {
235
- if (spine.workspaceRootContext.resolution === "unknown") {
236
- return undefined;
237
- }
238
- return path.join(spine.workspaceRootContext.runtimeRoot, SETUP_MARKER_RELATIVE_PATH);
239
- }
240
- function readSetupAckMarker(spine) {
241
- const markerPath = resolveSetupMarkerPath(spine);
242
- if (!markerPath) {
243
- return { status: "workspace_root_unknown" };
244
- }
245
- if (!fs.existsSync(markerPath)) {
246
- return { status: "pending", markerPath };
247
- }
248
- try {
249
- const marker = JSON.parse(fs.readFileSync(markerPath, "utf-8"));
250
- return {
251
- status: "acknowledged",
252
- markerPath,
253
- acknowledgedAt: marker.acknowledgedAt,
254
- placedIn: marker.placedIn,
255
- };
256
- }
257
- catch {
258
- return { status: "pending", markerPath };
259
- }
260
- }
261
- function readPackagedSetupText(fileName) {
262
- const fullPath = path.join(getPluginPackageRoot(), fileName);
263
- try {
264
- return {
265
- ok: true,
266
- path: fileName,
267
- content: fs.readFileSync(fullPath, "utf-8"),
268
- };
269
- }
270
- catch (error) {
271
- return {
272
- ok: false,
273
- path: fileName,
274
- error: error instanceof Error ? error.message : String(error),
275
- };
276
- }
277
- }
278
- function summarizeSetupText(content) {
279
- const lines = content
280
- .split(/\r?\n/)
281
- .map((line) => line.trim())
282
- .filter((line) => line && !line.startsWith("#"));
283
- return lines.slice(0, 6).join("\n");
284
- }
285
- function buildSetupNudge(spine) {
286
- const ack = readSetupAckMarker(spine);
287
- if (ack.status === "acknowledged") {
288
- return undefined;
289
- }
290
- return {
291
- status: ack.status,
292
- command: "setup_hint",
293
- ackCommand: "setup_ack",
294
- message: "Second Nature has an unread agent guide. Run setup_hint, read the returned SKILL and guide, place that guidance into the agent's working anchors, then run setup_ack.",
295
- markerPath: ack.markerPath,
296
- requiredUserInput: ack.status === "workspace_root_unknown"
297
- ? ["SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot"]
298
- : [],
299
- };
300
- }
301
- function withSetupNudge(spine, command, payload) {
302
- if (SETUP_COMMANDS.has(command) || payload.setupNudge !== undefined) {
303
- return payload;
304
- }
305
- const setupNudge = buildSetupNudge(spine);
306
- return setupNudge ? { ...payload, setupNudge } : payload;
307
- }
308
- function buildSetupHintPayload(spine, input) {
309
- const format = input?.format === "full" ? "full" : "summary";
310
- const includeSkill = input?.includeSkill !== false;
311
- const includeGuide = input?.includeGuide !== false;
312
- const ack = readSetupAckMarker(spine);
313
- const data = {
314
- status: ack.status,
315
- workspaceRootResolution: spine.workspaceRootContext.resolution,
316
- markerPath: ack.markerPath,
317
- acknowledgedAt: ack.acknowledgedAt,
318
- placedIn: ack.placedIn,
319
- recommendedPlacement: [
320
- "agent prompt",
321
- "workspace/IDENTITY.md",
322
- "workspace/USER.md",
323
- ],
324
- nextStep: ack.status === "acknowledged"
325
- ? "setup_already_acknowledged"
326
- : "read_returned_guidance_then_run_setup_ack",
327
- };
328
- if (includeSkill) {
329
- const skill = readPackagedSetupText("SKILL.md");
330
- data.skill = skill.ok
331
- ? {
332
- path: skill.path,
333
- content: format === "full" ? skill.content : summarizeSetupText(skill.content),
334
- }
335
- : skill;
336
- }
337
- if (includeGuide) {
338
- const guide = readPackagedSetupText("agent-inner-guide.md");
339
- data.guide = guide.ok
340
- ? {
341
- path: guide.path,
342
- content: format === "full" ? guide.content : summarizeSetupText(guide.content),
343
- }
344
- : guide;
345
- }
346
- return {
347
- ok: true,
348
- command: "setup_hint",
349
- surfaceMode: "host_safe_carrier",
350
- message: "Read the SKILL and guide as a friendly setup note, then place the guidance where the agent naturally checks its working anchors.",
351
- data,
352
- };
353
- }
354
- function buildSetupAckPayload(spine, input) {
355
- const markerPath = resolveSetupMarkerPath(spine);
356
- if (!markerPath) {
357
- return {
358
- ok: false,
359
- command: "setup_ack",
360
- error: {
361
- code: "SETUP_ACK_REQUIRES_WORKSPACE_ROOT",
362
- message: "setup_ack needs a workspace root so the one-shot marker can be persisted.",
363
- requiredUserInput: ["SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot"],
364
- nextStep: "reinvoke_setup_ack_with_workspace_root",
365
- },
366
- };
367
- }
368
- const marker = {
369
- acknowledgedAt: new Date().toISOString(),
370
- acceptedBy: safeShortText(input?.acceptedBy, 80) ?? "agent",
371
- placedIn: safeShortText(input?.placedIn, 160) ?? "unspecified",
372
- note: safeShortText(input?.note, 240),
373
- guideVersion: SETUP_GUIDE_VERSION,
374
- source: "second-nature-plugin",
375
- skillPath: "SKILL.md",
376
- guidePath: "agent-inner-guide.md",
377
- };
378
- fs.mkdirSync(path.dirname(markerPath), { recursive: true });
379
- fs.writeFileSync(markerPath, `${JSON.stringify(marker, null, 2)}\n`, "utf-8");
380
- return {
381
- ok: true,
382
- command: "setup_ack",
383
- surfaceMode: "host_safe_carrier",
384
- message: "Setup guide acknowledgement persisted; setup nudge is now silent for this workspace.",
385
- data: {
386
- markerPath,
387
- ...marker,
388
- },
389
- };
390
- }
391
- function parseExplainSubject(subjectRaw) {
392
- const trimmed = subjectRaw.trim();
393
- if (!trimmed) {
394
- throw new Error("explain_subject_invalid");
395
- }
396
- const separatorIndex = trimmed.indexOf(":");
397
- if (separatorIndex === -1) {
398
- throw new Error("explain_subject_requires_id");
399
- }
400
- const kind = trimmed.slice(0, separatorIndex).trim();
401
- const id = trimmed.slice(separatorIndex + 1).trim();
402
- if (!id) {
403
- throw new Error("explain_subject_requires_id");
404
- }
405
- switch (kind) {
406
- case "decision":
407
- return { subjectType: "decision", subjectId: id };
408
- case "platform":
409
- case "platform-selection":
410
- return { subjectType: "platform-selection", subjectId: id };
411
- case "outreach":
412
- return { subjectType: "outreach", subjectId: id };
413
- case "soul":
414
- case "soul-change":
415
- return { subjectType: "soul-change", subjectId: id };
416
- case "fallback":
417
- return { subjectType: "fallback", subjectId: id };
418
- case "probe":
419
- return { subjectType: "probe", subjectId: id };
420
- case "report":
421
- return { subjectType: "report", subjectId: id };
422
- case "delivery":
423
- return { subjectType: "delivery", subjectId: id };
424
- case "source":
425
- case "source_ref":
426
- return { subjectType: "source_ref", subjectId: id };
427
- default:
428
- throw new Error("explain_subject_unsupported");
429
- }
430
- }
431
- function buildStatusPayload(spine) {
432
- const runtimeEvidence = latestRuntimeEvidence(spine);
433
- const updatedAt = runtimeEvidence?.createdAt ??
434
- new Date(spine.lifecycleState.lastChangedAt).toISOString();
435
- const wr = spine.workspaceRootContext;
436
- const needsRootHint = wr.resolution === "unknown";
437
- return {
438
- ok: false,
439
- surfaceMode: "host_safe_carrier",
440
- workspaceReadModelsEvaluated: false,
441
- message: HOST_SAFE_LIMITATION_MESSAGE,
442
- error: {
443
- code: "WORKSPACE_READ_SURFACE_UNAVAILABLE",
444
- message: "Aggregated status requires workspace state; the host-safe plugin does not load persisted read models on this surface.",
445
- requiredUserInput: needsRootHint
446
- ? ["SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot"]
447
- : [],
448
- nextStep: "run_workspace_second_nature_cli_or_full_runtime_package",
449
- },
450
- data: {
451
- workspaceRootResolution: wr.resolution,
452
- carrier: {
453
- host: "openclaw-plugin",
454
- serviceStatus: spine.runtimeHandle.ready ? "running" : "idle",
455
- updatedAt,
456
- lastRuntimeTraceId: runtimeEvidence?.traceId,
457
- },
458
- },
459
- };
460
- }
461
- function buildQuietPayload(spine, scope) {
462
- const wr = spine.workspaceRootContext;
463
- return {
464
- ok: false,
465
- surfaceMode: "host_safe_carrier",
466
- workspaceReadModelsEvaluated: false,
467
- message: HOST_SAFE_LIMITATION_MESSAGE,
468
- error: {
469
- code: "QUIET_READ_SURFACE_UNAVAILABLE",
470
- message: "Quiet read surface requires workspace runtime; not evaluated in host-safe carrier mode.",
471
- requiredUserInput: wr.resolution === "unknown"
472
- ? ["SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot"]
473
- : [],
474
- nextStep: "run_workspace_second_nature_cli_or_full_runtime_package",
475
- },
476
- data: {
477
- scope,
478
- evaluated: false,
479
- unavailableReason: "host_safe_carrier_no_workspace_db",
480
- workspaceRootResolution: wr.resolution,
481
- },
482
- };
483
- }
484
- function buildReportPayload(spine, day) {
485
- const wr = spine.workspaceRootContext;
486
- return {
487
- ok: false,
488
- surfaceMode: "host_safe_carrier",
489
- workspaceReadModelsEvaluated: false,
490
- message: HOST_SAFE_LIMITATION_MESSAGE,
491
- error: {
492
- code: "REPORT_READ_SURFACE_UNAVAILABLE",
493
- message: "Daily report artifacts require workspace runtime.",
494
- requiredUserInput: wr.resolution === "unknown"
495
- ? ["SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot"]
496
- : [],
497
- nextStep: "run_workspace_second_nature_cli_or_full_runtime_package",
498
- },
499
- data: {
500
- evaluated: false,
501
- unavailableReason: "host_safe_carrier_no_workspace_db",
502
- day: day && day.trim() ? day : new Date().toISOString().slice(0, 10),
503
- workspaceRootResolution: wr.resolution,
504
- },
505
- };
506
- }
507
- function buildSessionPayload(spine, sessionId) {
508
- if (!sessionId) {
509
- return {
510
- ok: false,
511
- error: {
512
- code: "MISSING_SESSION_ID",
513
- message: "session show requires sessionId",
514
- requiredUserInput: ["session_id"],
515
- nextStep: "reinvoke_session_with_session_id",
516
- },
517
- };
518
- }
519
- const wr = spine.workspaceRootContext;
520
- return {
521
- ok: false,
522
- surfaceMode: "host_safe_carrier",
523
- workspaceReadModelsEvaluated: false,
524
- message: HOST_SAFE_LIMITATION_MESSAGE,
525
- error: {
526
- code: "SESSION_READ_SURFACE_UNAVAILABLE",
527
- message: "Session analytics require workspace state database.",
528
- requiredUserInput: wr.resolution === "unknown"
529
- ? ["SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot"]
530
- : [],
531
- nextStep: "run_workspace_second_nature_cli_or_full_runtime_package",
532
- },
533
- data: {
534
- requestedSessionId: sessionId,
535
- evaluated: false,
536
- unavailableReason: "host_safe_carrier_no_workspace_db",
537
- workspaceRootResolution: wr.resolution,
538
- },
539
- };
540
- }
541
- function buildCredentialPayload(spine, platformId) {
542
- const wr = spine.workspaceRootContext;
543
- return {
544
- ok: false,
545
- surfaceMode: "host_safe_carrier",
546
- workspaceReadModelsEvaluated: false,
547
- message: HOST_SAFE_LIMITATION_MESSAGE,
548
- error: {
549
- code: "CREDENTIAL_READ_SURFACE_UNAVAILABLE",
550
- message: "Credential inspection requires workspace runtime on this surface.",
551
- requiredUserInput: wr.resolution === "unknown"
552
- ? ["SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot"]
553
- : [],
554
- nextStep: "run_workspace_second_nature_cli_or_full_runtime_package",
555
- },
556
- data: {
557
- platformId: platformId && platformId.trim() ? platformId : undefined,
558
- evaluated: false,
559
- unavailableReason: "host_safe_carrier_no_workspace_db",
560
- workspaceRootResolution: wr.resolution,
561
- },
562
- };
563
- }
564
- function buildExplainPayload(spine, subjectRaw) {
565
- if (!subjectRaw?.trim()) {
566
- return {
567
- ok: false,
568
- error: {
569
- code: "MISSING_EXPLAIN_SUBJECT",
570
- message: "explain requires subject",
571
- requiredUserInput: ["subject"],
572
- nextStep: "reinvoke_explain_with_subject",
573
- },
574
- };
575
- }
576
- let subject;
577
- try {
578
- subject = parseExplainSubject(subjectRaw);
579
- }
580
- catch (error) {
581
- const code = error.message;
582
- if (code === "explain_subject_requires_id") {
583
- return createUnavailableActionError("EXPLAIN_SUBJECT_REQUIRES_ID", "subject must include identifier", ["subject"], "reinvoke_explain_with_supported_subject");
584
- }
585
- if (code === "explain_subject_unsupported") {
586
- return createUnavailableActionError("EXPLAIN_SUBJECT_UNSUPPORTED", "supported subjects include decision:, platform:, outreach:, soul:, fallback:, delivery:, probe:, report:, source:", ["subject"], "reinvoke_explain_with_supported_subject");
587
- }
588
- return createUnavailableActionError("EXPLAIN_SUBJECT_INVALID", "invalid explain subject", ["subject"], "reinvoke_explain_with_supported_subject");
589
- }
590
- const wr = spine.workspaceRootContext;
591
- return {
592
- ok: false,
593
- surfaceMode: "host_safe_carrier",
594
- workspaceReadModelsEvaluated: false,
595
- message: HOST_SAFE_LIMITATION_MESSAGE,
596
- error: {
597
- code: "EXPLAIN_READ_SURFACE_UNAVAILABLE",
598
- message: "Evidence-backed explain requires persisted workspace read models; host-safe carrier did not evaluate operator explain (CH-11-02).",
599
- requiredUserInput: wr.resolution === "unknown"
600
- ? ["SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot"]
601
- : [],
602
- nextStep: "run_workspace_second_nature_cli_or_full_runtime_package",
603
- },
604
- data: {
605
- subjectType: subject.subjectType,
606
- evaluated: false,
607
- workspaceRootResolution: wr.resolution,
608
- },
609
- };
610
- }
611
- async function buildStorageSmokePayload(input) {
612
- try {
613
- const mod = await import("./runtime/storage/bootstrap/storage-mode-smoke.js");
614
- const runRepairFixture = Boolean(input?.runRepairFixture);
615
- const workspaceRoot = typeof input?.workspaceRoot === "string"
616
- ? input.workspaceRoot
617
- : undefined;
618
- const data = await mod.runStorageModeSmoke({
619
- runRepairFixture,
620
- workspaceRoot,
621
- });
622
- return { ok: true, data };
623
- }
624
- catch (error) {
625
- return {
626
- ok: false,
627
- message: error instanceof Error ? error.message : String(error),
628
- error: {
629
- code: "STORAGE_SMOKE_LOAD_FAILED",
630
- message: "Could not load packaged storage-mode smoke module",
631
- nextStep: "rebuild_plugin_runtime_package",
632
- },
633
- };
634
- }
635
- }
636
- function buildFallbackHostSafePayload(ref) {
637
- if (!ref?.trim()) {
638
- return {
639
- ok: false,
640
- error: {
641
- code: "MISSING_FALLBACK_REF",
642
- message: "fallback requires ref (e.g. fallback:…)",
643
- requiredUserInput: ["ref"],
644
- nextStep: "reinvoke_with_ref",
645
- },
646
- };
647
- }
648
- 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");
649
- }
650
- function isProbeOnlyInput(input) {
651
- const v = input?.probeOnly;
652
- return v === true || v === "true" || v === 1 || v === "1";
653
- }
654
- function buildHeartbeatCheckPayload(spine, input) {
655
- const runtimeEvidence = latestRuntimeEvidence(spine);
656
- const updatedAt = runtimeEvidence?.createdAt ??
657
- new Date(spine.lifecycleState.lastChangedAt).toISOString();
658
- const timestamp = typeof input?.timestamp === "string" && input.timestamp.trim().length > 0
659
- ? input.timestamp
660
- : updatedAt;
661
- const wr = spine.workspaceRootContext;
662
- if (isProbeOnlyInput(input)) {
663
- return {
664
- ok: true,
665
- status: "heartbeat_ok",
666
- surfaceMode: "capability_probe",
667
- reasons: ["probe_only"],
668
- livedExperienceLoopClaimed: false,
669
- scope: "rhythm",
670
- trigger: "heartbeat_bridge",
671
- message: "Capability probe only on the host-safe carrier surface; does not claim a full lived-experience decision loop.",
672
- data: {
673
- workspaceRootResolution: wr.resolution,
674
- runtime: {
675
- host: "openclaw-plugin",
676
- serviceStatus: spine.runtimeHandle.ready ? "running" : "idle",
677
- updatedAt,
678
- },
679
- surface: {
680
- tool: "second_nature_ops",
681
- command: "second-nature heartbeat_check",
682
- },
683
- bridge: {
684
- timestamp,
685
- probeOnly: true,
686
- sessionContextProvided: typeof input?.sessionContext === "string" &&
687
- input.sessionContext.trim().length > 0,
688
- heartbeatChecklistProvided: typeof input?.heartbeatChecklist === "string" &&
689
- input.heartbeatChecklist.trim().length > 0,
690
- serviceEntryMode: "capability_probe",
691
- },
692
- },
693
- };
694
- }
695
- return {
696
- ok: true,
697
- status: "runtime_carrier_only",
698
- surfaceMode: "host_safe_carrier",
699
- livedExperienceLoopClaimed: false,
700
- scope: "rhythm",
701
- trigger: "heartbeat_bridge",
702
- reasons: ["runtime_carrier_only", "host_safe_bridge_ack"],
703
- nextAction: "continue_carrier_surface_only",
704
- 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.",
705
- data: {
706
- workspaceRootResolution: wr.resolution,
707
- runtime: {
708
- host: "openclaw-plugin",
709
- serviceStatus: spine.runtimeHandle.ready ? "running" : "idle",
710
- updatedAt,
711
- },
712
- surface: {
713
- tool: "second_nature_ops",
714
- command: "second-nature heartbeat_check",
715
- },
716
- bridge: {
717
- timestamp,
718
- sessionContextProvided: typeof input?.sessionContext === "string" &&
719
- input.sessionContext.trim().length > 0,
720
- heartbeatChecklistProvided: typeof input?.heartbeatChecklist === "string" &&
721
- input.heartbeatChecklist.trim().length > 0,
722
- serviceEntryMode: "runtime_carrier_only",
723
- },
724
- },
725
- };
726
- }
727
- function createHostSafeRouter(spine) {
728
- const commands = [
729
- {
730
- name: "status",
731
- description: "Show aggregated Second Nature status",
732
- execute: async () => buildStatusPayload(spine),
733
- },
734
- {
735
- name: "setup_hint",
736
- description: "Return the packaged setup SKILL and agent inner guide for first-run onboarding",
737
- execute: async (input) => buildSetupHintPayload(spine, input),
738
- },
739
- {
740
- name: "setup_ack",
741
- description: "Persist that the packaged setup guide was read and placed into working anchors",
742
- execute: async (input) => buildSetupAckPayload(spine, input),
743
- },
744
- {
745
- name: "policy",
746
- description: "Write or inspect policy state",
747
- execute: async (input) => {
748
- const action = typeof input?.action === "string" ? input.action : "show";
749
- if (action === "set") {
750
- 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");
751
- }
752
- return createUnavailableActionError("HOST_SAFE_POLICY_SHOW_UNAVAILABLE", "Policy read requires workspace state database; host-safe plugin does not load persisted policy rows.", [], "run_workspace_second_nature_cli_or_full_runtime_package");
753
- },
754
- },
755
- {
756
- name: "credential",
757
- description: "Inspect or recover credential state",
758
- execute: async (input) => {
759
- const action = typeof input?.action === "string" ? input.action : "show";
760
- if (action === "verify") {
761
- 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");
762
- }
763
- const platformId = typeof input?.platformId === "string" ? input.platformId : undefined;
764
- return buildCredentialPayload(spine, platformId);
765
- },
766
- },
767
- {
768
- name: "quiet",
769
- description: "Inspect Quiet lifecycle state",
770
- execute: async (input) => {
771
- const scope = typeof input?.scope === "string" ? input.scope : undefined;
772
- return buildQuietPayload(spine, scope);
773
- },
774
- },
775
- {
776
- name: "report",
777
- description: "Show daily report artifacts",
778
- execute: async (input) => {
779
- const day = typeof input?.day === "string" ? input.day : undefined;
780
- return buildReportPayload(spine, day);
781
- },
782
- },
783
- {
784
- name: "session",
785
- description: "Inspect continuity session details",
786
- execute: async (input) => {
787
- const sessionId = typeof input?.sessionId === "string" ? input.sessionId : undefined;
788
- return buildSessionPayload(spine, sessionId);
789
- },
790
- },
791
- {
792
- name: "audit",
793
- description: "Inspect audit and evidence views",
794
- execute: async () => createUnavailableActionError("HOST_SAFE_AUDIT_UNAVAILABLE", "Audit read requires workspace observability database; host-safe plugin does not load persisted audit events.", [], "run_workspace_second_nature_cli_or_full_runtime_package"),
795
- },
796
- {
797
- name: "explain",
798
- description: "Answer why-question explain requests",
799
- execute: async (input) => {
800
- const subject = typeof input?.subject === "string" ? input.subject : undefined;
801
- return buildExplainPayload(spine, subject);
802
- },
803
- },
804
- {
805
- name: "heartbeat_check",
806
- description: "Acknowledge the shipping heartbeat bridge round",
807
- execute: async (input) => buildHeartbeatCheckPayload(spine, input),
808
- },
809
- {
810
- name: "fallback",
811
- description: "Operator-visible delivery fallback view (full workspace runtime required)",
812
- execute: async (input) => {
813
- const ref = typeof input?.ref === "string" ? input.ref.trim() : undefined;
814
- return buildFallbackHostSafePayload(ref);
815
- },
816
- },
817
- {
818
- name: "storage_smoke",
819
- description: "T4.1.4 storage mode smoke report (sql.js vs native probe)",
820
- execute: async (input) => buildStorageSmokePayload(input),
821
- },
822
- {
823
- name: "capability_probe",
824
- description: "Probe host capabilities (workspace runtime required for full report)",
825
- execute: async () => createUnavailableActionError("HOST_SAFE_CAPABILITY_PROBE_UNAVAILABLE", "Full capability probe requires workspace observability database for persistence; host-safe carrier returns static unknown.", [], "run_workspace_second_nature_cli_or_full_runtime_package"),
826
- },
827
- {
828
- name: "near_real_smoke",
829
- description: "Run near-real connector smoke (workspace runtime + connectors required)",
830
- execute: async () => createUnavailableActionError("HOST_SAFE_NEAR_REAL_SMOKE_UNAVAILABLE", "Near-real connector smoke requires workspace state and observability databases; host-safe plugin cannot run connector harness.", [], "run_workspace_second_nature_cli_or_full_runtime_package"),
831
- },
832
- // v6 ops surface (CR8-01): host-safe router returns unavailable for workspace-only commands
833
- {
834
- name: "narrative",
835
- description: "Show current NarrativeState (workspace runtime required)",
836
- execute: async () => createUnavailableActionError("HOST_SAFE_NARRATIVE_UNAVAILABLE", "NarrativeState read requires workspace state database; host-safe plugin does not load persisted narrative rows.", [], "run_workspace_second_nature_cli_or_full_runtime_package"),
837
- },
838
- {
839
- name: "goal",
840
- description: "Owner-governed goal operations (workspace runtime required)",
841
- execute: async (input) => {
842
- const action = typeof input?.action === "string" ? input.action : "list";
843
- if (action === "set" || action === "accept" || action === "reject") {
844
- return createUnavailableActionError("HOST_SAFE_GOAL_MUTATE_UNAVAILABLE", "Goal mutation requires workspace state database; host-safe plugin cannot write persisted goal rows.", [], "run_workspace_second_nature_cli_or_full_runtime_package");
845
- }
846
- return createUnavailableActionError("HOST_SAFE_GOAL_READ_UNAVAILABLE", "Goal list/read requires workspace state database; host-safe plugin does not load persisted goal rows.", [], "run_workspace_second_nature_cli_or_full_runtime_package");
847
- },
848
- },
849
- {
850
- name: "dream:recent",
851
- description: "Show recent Dream runs (workspace runtime required)",
852
- execute: async () => createUnavailableActionError("HOST_SAFE_DREAM_RECENT_UNAVAILABLE", "Dream recent read requires workspace observability database; host-safe plugin does not load persisted audit events.", [], "run_workspace_second_nature_cli_or_full_runtime_package"),
853
- },
854
- {
855
- name: "connector_status",
856
- description: "Show connector inventory (workspace runtime required)",
857
- execute: async () => createUnavailableActionError("HOST_SAFE_CONNECTOR_STATUS_UNAVAILABLE", "Connector status requires workspace state and registry scan; host-safe plugin cannot access connector manifests.", [], "run_workspace_second_nature_cli_or_full_runtime_package"),
858
- },
859
- {
860
- name: "connector_test",
861
- description: "Dry-run test a connector (workspace runtime required)",
862
- execute: async () => createUnavailableActionError("HOST_SAFE_CONNECTOR_TEST_UNAVAILABLE", "Connector test requires workspace state and registry; host-safe plugin cannot run connector harness.", [], "run_workspace_second_nature_cli_or_full_runtime_package"),
863
- },
864
- {
865
- name: "connector_behavior_add",
866
- description: "Add a workspace connector behavior declaration (workspace runtime required)",
867
- execute: async () => createUnavailableActionError("HOST_SAFE_CONNECTOR_BEHAVIOR_ADD_UNAVAILABLE", "Connector behavior authoring writes workspace manifests; host-safe plugin cannot mutate connector files.", ["platformId", "behaviorId"], "run_workspace_second_nature_cli_or_full_runtime_package"),
868
- },
869
- {
870
- name: "cycle:recent",
871
- description: "Show recent cycle summary (workspace runtime required)",
872
- execute: async () => createUnavailableActionError("HOST_SAFE_CYCLE_RECENT_UNAVAILABLE", "Cycle recent read requires workspace observability database; host-safe plugin does not load persisted audit events.", [], "run_workspace_second_nature_cli_or_full_runtime_package"),
873
- },
874
- ];
875
- return {
876
- commands,
877
- resolve(name) {
878
- return commands.find((command) => command.name === name);
879
- },
880
- };
881
- }
882
- function createActivationSpine() {
883
- const workspaceRootContext = resolveWorkspaceRoot(undefined);
884
- const spine = {
885
- router: undefined,
886
- runtimeHandle: startRuntimeService({
887
- workspaceRoot: workspaceRootContext.runtimeRoot,
888
- }),
889
- lifecycleState: getLifecycleState(),
890
- serviceStartRecorded: false,
891
- runtimeEvidence: [],
892
- workspaceRootContext,
893
- };
894
- spine.router = createHostSafeRouter(spine);
895
- return spine;
896
- }
897
- function ensureActivationSpine() {
898
- if (activationSpine) {
899
- return activationSpine;
900
- }
901
- activationSpine = createActivationSpine();
902
- return activationSpine;
903
- }
904
- function recordRuntimeEvidence(spine, origin) {
905
- if (origin === "service_start" && spine.serviceStartRecorded) {
906
- return;
907
- }
908
- if (origin === "service_start") {
909
- spine.serviceStartRecorded = true;
910
- }
911
- spine.runtimeEvidence.push({
912
- traceId: `${INTERNAL_RUNTIME_TRACE_PREFIX}${origin}-${spine.lifecycleState.registerCount}-${Date.now()}`,
913
- capability: origin === "register"
914
- ? spine.lifecycleState.registerCount === 1
915
- ? "runtime.activate"
916
- : "runtime.reload"
917
- : "runtime.heartbeat",
918
- origin,
919
- createdAt: new Date().toISOString(),
920
- status: "succeeded",
921
- });
922
- trimRuntimeEvidence(spine);
923
- }
924
- function refreshRegistrationState() {
925
- const spine = ensureActivationSpine();
926
- const workspaceRootContext = resolveWorkspaceRoot(undefined);
927
- const prev = spine.workspaceRootContext;
928
- const changed = workspaceRootContext.runtimeRoot !== prev.runtimeRoot ||
929
- workspaceRootContext.resolution !== prev.resolution;
930
- if (changed) {
931
- disposeWorkspaceOpsBridge();
932
- }
933
- spine.workspaceRootContext = workspaceRootContext;
934
- spine.runtimeHandle = startRuntimeService({
935
- workspaceRoot: workspaceRootContext.runtimeRoot,
936
- });
937
- spine.lifecycleState = recordRegistration();
938
- spine.serviceStartRecorded = false;
939
- recordRuntimeEvidence(spine, "register");
940
- return spine;
941
- }
942
- function parseCommandInput(rawArgs) {
943
- const tokens = rawArgs?.trim().split(/\s+/).filter(Boolean) ?? [];
944
- if (tokens.length === 0) {
945
- return {
946
- ok: false,
947
- result: { ok: false, message: "Missing command argument." },
948
- };
949
- }
950
- const [command, ...rest] = tokens;
951
- if (command === "policy" && rest[0] === "set") {
952
- return {
953
- ok: false,
954
- result: {
955
- ok: false,
956
- command,
957
- message: "policy set requires structured args; use second_nature_ops instead.",
958
- },
959
- };
960
- }
961
- if (command === "credential" && rest[0] === "verify") {
962
- return {
963
- ok: false,
964
- result: {
965
- ok: false,
966
- command,
967
- message: "credential verify requires structured args; use second_nature_ops instead.",
968
- },
969
- };
970
- }
971
- switch (command) {
972
- case "setup_hint":
973
- return {
974
- ok: true,
975
- command,
976
- input: rest.includes("--full") ? { format: "full" } : undefined,
977
- };
978
- case "setup_ack":
979
- return {
980
- ok: true,
981
- command,
982
- input: rest.length > 0
983
- ? { acceptedBy: rest[0], placedIn: rest.slice(1).join(" ") }
984
- : undefined,
985
- };
986
- case "status":
987
- case "quiet":
988
- return {
989
- ok: true,
990
- command,
991
- input: rest.length > 0 ? { scope: rest.join(" ") } : undefined,
992
- };
993
- case "report":
994
- return {
995
- ok: true,
996
- command,
997
- input: rest[0] ? { day: rest[0] } : undefined,
998
- };
999
- case "session":
1000
- return {
1001
- ok: true,
1002
- command,
1003
- input: rest[0] ? { sessionId: rest[0] } : undefined,
1004
- };
1005
- case "credential":
1006
- return {
1007
- ok: true,
1008
- command,
1009
- input: rest[0] ? { platformId: rest[0] } : undefined,
1010
- };
1011
- case "heartbeat_check":
1012
- return {
1013
- ok: true,
1014
- command,
1015
- input: rest.length > 0
1016
- ? {
1017
- timestamp: rest[0],
1018
- sessionContext: rest.length > 1 ? rest.slice(1).join(" ") : undefined,
1019
- }
1020
- : undefined,
1021
- };
1022
- case "explain":
1023
- return {
1024
- ok: true,
1025
- command,
1026
- input: rest.length > 0 ? { subject: rest.join(" ") } : undefined,
1027
- };
1028
- case "fallback":
1029
- return {
1030
- ok: true,
1031
- command,
1032
- input: rest.length > 0 ? { ref: rest.join(" ") } : undefined,
1033
- };
1034
- case "storage_smoke": {
1035
- const wantRepair = rest[0] === "repair" || rest.includes("--repair");
1036
- return {
1037
- ok: true,
1038
- command,
1039
- input: wantRepair ? { runRepairFixture: true } : undefined,
1040
- };
1041
- }
1042
- // v6 ops surface (CR8-01): simple command parsing for new commands
1043
- case "narrative":
1044
- return {
1045
- ok: true,
1046
- command,
1047
- input: rest[0] ? { narrativeId: rest[0] } : undefined,
1048
- };
1049
- case "goal":
1050
- return {
1051
- ok: true,
1052
- command,
1053
- input: rest.length > 0 ? { action: rest[0], goalId: rest[1] } : undefined,
1054
- };
1055
- case "dream:recent":
1056
- return {
1057
- ok: true,
1058
- command,
1059
- input: rest[0] ? { limit: Number(rest[0]) } : undefined,
1060
- };
1061
- case "connector_status":
1062
- return { ok: true, command, input: undefined };
1063
- case "connector_test":
1064
- return {
1065
- ok: true,
1066
- command,
1067
- input: rest[0] ? { platformId: rest[0] } : undefined,
1068
- };
1069
- case "connector_behavior_add":
1070
- return {
1071
- ok: true,
1072
- command,
1073
- input: rest.length > 1
1074
- ? { platformId: rest[0], behaviorId: rest[1], description: rest.slice(2).join(" ") }
1075
- : undefined,
1076
- };
1077
- case "cycle:recent":
1078
- return {
1079
- ok: true,
1080
- command,
1081
- input: rest[0] ? { limit: Number(rest[0]) } : undefined,
1082
- };
1083
- // v7 ops surface (T-ROS.C.2)
1084
- case "self_health":
1085
- return { ok: true, command, input: undefined };
1086
- case "tool_affordance":
1087
- return {
1088
- ok: true,
1089
- command,
1090
- input: rest[0] ? { query: rest.join(" ") } : undefined,
1091
- };
1092
- case "heartbeat_digest":
1093
- return {
1094
- ok: true,
1095
- command,
1096
- input: rest[0] ? { date: rest[0] } : undefined,
1097
- };
1098
- case "snapshot:capture":
1099
- return {
1100
- ok: true,
1101
- command,
1102
- input: rest[0] ? { snapshotId: rest[0] } : undefined,
1103
- };
1104
- case "narrative:diff":
1105
- return {
1106
- ok: true,
1107
- command,
1108
- input: rest.length >= 2 ? { from: rest[0], to: rest[1] } : undefined,
1109
- };
1110
- case "timeline":
1111
- return {
1112
- ok: true,
1113
- command,
1114
- input: rest[0] ? { limit: Number(rest[0]) } : undefined,
1115
- };
1116
- case "restore":
1117
- return {
1118
- ok: true,
1119
- command,
1120
- // restore <restoreTarget> <fromVersion> <toVersion>
1121
- input: rest.length >= 3
1122
- ? { restoreTarget: rest[0], fromVersion: rest[1], toVersion: rest[2] }
1123
- : undefined,
1124
- };
1125
- case "runtime_secret_bootstrap":
1126
- return { ok: true, command, input: undefined };
1127
- case "connector:run":
1128
- return {
1129
- ok: true,
1130
- command,
1131
- // connector:run <platformId> <capabilityId> [payloadJson]
1132
- input: rest.length >= 2
1133
- ? {
1134
- platformId: rest[0],
1135
- capabilityId: rest[1],
1136
- payload: rest[2] ? JSON.parse(rest[2]) : undefined,
1137
- }
1138
- : undefined,
1139
- };
1140
- default:
1141
- return {
1142
- ok: true,
1143
- command,
1144
- input: undefined,
1145
- };
1146
- }
1147
- }
1148
- function createRuntimeService() {
1149
- return {
1150
- id: "second-nature-runtime",
1151
- start() {
1152
- const spine = ensureActivationSpine();
1153
- recordRuntimeEvidence(spine, "service_start");
1154
- return {
1155
- ready: spine.runtimeHandle.ready,
1156
- version: spine.runtimeHandle.version,
1157
- };
1158
- },
1159
- };
1160
- }
1161
- function createLifecycleService() {
1162
- return {
1163
- id: "second-nature-lifecycle",
1164
- start() {
1165
- const spine = ensureActivationSpine();
1166
- return {
1167
- phase: spine.lifecycleState.phase,
1168
- registerCount: spine.lifecycleState.registerCount,
1169
- lastChangedAt: spine.lifecycleState.lastChangedAt,
1170
- };
1171
- },
1172
- };
1173
- }
1174
- const SECOND_NATURE_TOOL_SCHEMA = {
1175
- type: "object",
1176
- additionalProperties: false,
1177
- properties: {
1178
- command: { type: "string" },
1179
- args: { type: "object", additionalProperties: true },
1180
- workspaceRoot: {
1181
- type: "string",
1182
- description: "Workspace root for packaged smoke/runtime alignment (optional; prefer SECOND_NATURE_WORKSPACE_ROOT).",
1183
- },
1184
- },
1185
- required: ["command"],
1186
- };
1187
- export default {
1188
- id: "second-nature",
1189
- name: "Second Nature",
1190
- description: "Registers command/tool/service surface with load-reload lifecycle semantics.",
1191
- register(api) {
1192
- process.stderr.write(`[second-nature] register() entered, api keys=${Object.keys(api).join(",")}\n`);
1193
- const runtimeService = createRuntimeService();
1194
- const lifecycleService = createLifecycleService();
1195
- api.registerService(runtimeService);
1196
- api.registerService(lifecycleService);
1197
- api.registerCommand({
1198
- name: "second-nature",
1199
- description: "Route Agent-facing operational commands for Second Nature.",
1200
- acceptsArgs: true,
1201
- handler: async (ctx) => {
1202
- const spine = ensureActivationSpine();
1203
- const parsed = parseCommandInput(ctx.args);
1204
- if (!parsed.ok) {
1205
- return {
1206
- text: JSON.stringify(parsed.result),
1207
- };
1208
- }
1209
- const resolved = spine.router.resolve(parsed.command);
1210
- if (!resolved && !isWorkspaceBridgeCommand(parsed.command, parsed.input)) {
1211
- return {
1212
- text: JSON.stringify({
1213
- ok: false,
1214
- command: parsed.command,
1215
- message: "Unknown Second Nature command.",
1216
- }),
1217
- };
1218
- }
1219
- const result = await routeSecondNatureCommand(spine, parsed.command, parsed.input);
1220
- return {
1221
- text: JSON.stringify(result),
1222
- };
1223
- },
1224
- });
1225
- const executeSecondNatureTool = async (params) => {
1226
- const spine = ensureActivationSpine();
1227
- syncWorkspaceRootFromTool(spine, params.workspaceRoot);
1228
- const resolved = spine.router.resolve(params.command);
1229
- if (!resolved && !isWorkspaceBridgeCommand(params.command, params.args)) {
1230
- return {
1231
- content: [
1232
- {
1233
- type: "text",
1234
- text: JSON.stringify({
1235
- ok: false,
1236
- message: "Unknown Second Nature command.",
1237
- }),
1238
- },
1239
- ],
1240
- };
1241
- }
1242
- const result = await routeSecondNatureCommand(spine, params.command, params.args);
1243
- return {
1244
- content: [
1245
- {
1246
- type: "text",
1247
- text: JSON.stringify(result),
1248
- },
1249
- ],
1250
- };
1251
- };
1252
- api.registerTool({
1253
- name: "second_nature_ops",
1254
- description: "Access the Second Nature command surface through a single tool shell.",
1255
- parameters: SECOND_NATURE_TOOL_SCHEMA,
1256
- async execute(_id, params) {
1257
- return executeSecondNatureTool(params);
1258
- },
1259
- });
1260
- process.stderr.write("[second-nature] register() completed\n");
1261
- },
1262
- };
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
+ * - T4.2.1: owner reply ingestion → RelationshipMemory feedback (full runtime only)
11
+ *
12
+ * Dependencies:
13
+ * - only imports runtime lifecycle/service modules that are synchronous at load time
14
+ *
15
+ * Boundaries:
16
+ * - read-only operator flows stay available through command/tool surface
17
+ * - structured mutating flows such as policy set / credential verify remain unavailable here
18
+ * - full evidence-backed workspace runtime can be reintroduced later behind a host-safe boundary
19
+ *
20
+ * Plugin classification (verified against OpenClaw 2026.5.4 internals, see
21
+ * docs/validation/openclaw-plugin-classification.md and the explore reports
22
+ * dated 2026-05-06):
23
+ * - Second Nature is a TOOL plugin (exposes `second_nature_ops` to agent
24
+ * sessions). It is intentionally NOT a channel/provider/context-engine.
25
+ * - OpenClaw's `loadGatewayStartupPluginPlan` only loads plugins that opt in
26
+ * via `manifest.activation.onStartup === true`, occupy a configured slot
27
+ * (channel/contextEngine/provider), or declare an explicit hook intent. A
28
+ * tool-only plugin without `activation.onStartup` will be enabled in the
29
+ * registry yet never loaded by the gateway daemon — register(api) only fires
30
+ * inside the `openclaw plugins enable` CLI process, which produces the
31
+ * illusion of a working plugin while agent sessions see no tool. We hit
32
+ * exactly that on 2026-05-06; the fix lives in plugin/openclaw.plugin.json
33
+ * under the `activation` block.
34
+ * - `Shape: non-capability` reported by `openclaw plugins info` is EXPECTED
35
+ * for this plugin. OpenClaw counts capabilities only across cli-backend /
36
+ * text-inference / speech / realtime-* / media-understanding /
37
+ * image-generation / web-search / agent-harness / context-engine / channel.
38
+ * Tool/command/service contributions never bump that count. Pretending to
39
+ * be a context engine with a stub factory just to flip the label would lie
40
+ * to the host (ContextEngine.ingest/assemble/compact get called for real).
41
+ * When Second Nature ships a genuine context-engine layer in a future
42
+ * release, the shape will move to plain-capability honestly.
43
+ *
44
+ * OpenClaw operator norm (T1.1.4 / T1.1.5): set `SECOND_NATURE_WORKSPACE_ROOT` or tool `workspaceRoot` to the
45
+ * **same absolute path** as the OpenClaw **agent workspace** (default `~/.openclaw/workspace`, or
46
+ * `agents.defaults.workspace` in `~/.openclaw/openclaw.json`). Do **not** infer that root from the plugin
47
+ * 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`.
49
+ *
50
+ * Test coverage:
51
+ * - tests/integration/cli/plugin-runtime-registration.test.ts
52
+ * - tests/integration/cli/plugin-packaging-walkthrough.test.ts
53
+ * - tests/integration/cli/plugin-workspace-ops-bridge.test.ts (T1.1.4 / CH-13 matrix, T1.1.5 ops docs cross-ref)
54
+ */
55
+ import fs from "node:fs";
56
+ import path from "node:path";
57
+ import { fileURLToPath } from "node:url";
58
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
59
+ const PLUGIN_VERSION = JSON.parse(fs.readFileSync(path.resolve(__dirname, "package.json"), "utf-8")).version;
60
+ import { startRuntimeService, } from "./runtime/core/second-nature/runtime/service-entry.js";
61
+ import { getLifecycleState, recordRegistration, } from "./runtime/core/second-nature/runtime/lifecycle-service.js";
62
+ import { openWorkspaceOpsBridge } from "./workspace-ops-bridge.js";
63
+ // Keep the entry as a plain object instead of importing OpenClaw's
64
+ // SDK entry helper. Upload/package validators may import this module
65
+ // before the host SDK is installed; a static SDK import turns a valid package
66
+ // into ERR_MODULE_NOT_FOUND. OpenClaw classifies this plugin from the manifest
67
+ // fields, and the helper returns this same object shape at runtime.
68
+ // Stderr sentinels make daemon load-path observable in `gateway.log`. Three
69
+ // lines should appear at startup: "module evaluated", "register() entered ...",
70
+ // "register() completed". Their absence after `openclaw gateway run` proves
71
+ // the daemon never reached this entry — typically a manifest activation gap.
72
+ process.stderr.write("[second-nature] module evaluated\n");
73
+ const INTERNAL_RUNTIME_TRACE_PREFIX = "sn-runtime-";
74
+ const HOST_SAFE_LIMITATION_MESSAGE = "Host-safe plugin package keeps synchronous register/load semantics, but mutating workspace runtime flows remain unavailable here.";
75
+ const SETUP_MARKER_RELATIVE_PATH = path.join(".second-nature", "setup", "agent-inner-guide-ack.json");
76
+ const SETUP_GUIDE_VERSION = "0.1.38";
77
+ const SETUP_COMMANDS = new Set(["setup_hint", "setup_ack"]);
78
+ let activationSpine = null;
79
+ /** T1.1.4 — lazily opened full read bridge; closed when workspace root / resolution changes. */
80
+ let workspaceOpsBridge = null;
81
+ function disposeWorkspaceOpsBridge() {
82
+ if (workspaceOpsBridge) {
83
+ workspaceOpsBridge.close();
84
+ workspaceOpsBridge = null;
85
+ }
86
+ }
87
+ const WORKSPACE_BRIDGE_COMMANDS = new Set([
88
+ "status",
89
+ "quiet",
90
+ "report",
91
+ "session",
92
+ "explain",
93
+ "heartbeat_check",
94
+ "fallback",
95
+ "storage_smoke",
96
+ // T1.2.8 (SN-CODE-03): capability probe surface via workspace bridge
97
+ "capability_probe",
98
+ // T1.2.6 / T1.2.7: policy show + audit read surface
99
+ "policy",
100
+ "audit",
101
+ // T3.3.2: near-real connector smoke sentinel
102
+ "near_real_smoke",
103
+ // v6 ops surface (CR8-01): narrative, goal, dream:recent, connector_status/test, cycle:recent
104
+ "narrative",
105
+ "goal",
106
+ "dream:recent",
107
+ "connector_status",
108
+ "connector_test",
109
+ "connector_behavior_add",
110
+ "cycle:recent",
111
+ // v7 ops surface (T-ROS.C.1 / T-ROS.C.2 / T-ROS.C.3 / T-V7C.C.5): self_health, tool_affordance,
112
+ // heartbeat_digest, snapshot:capture, narrative:diff, timeline, restore, runtime_secret_bootstrap,
113
+ // connector:run, guidance_payload
114
+ "self_health",
115
+ "tool_affordance",
116
+ "heartbeat_digest",
117
+ "snapshot:capture",
118
+ "narrative:diff",
119
+ "timeline",
120
+ "restore",
121
+ "runtime_secret_bootstrap",
122
+ "connector:run",
123
+ // T-V7C.C.5: host ops surface parity guidance_payload must be whitelisted for Claw reachability
124
+ "guidance_payload",
125
+ ]);
126
+ function isWorkspaceBridgeCommand(command, input) {
127
+ if (command === "credential") {
128
+ const action = typeof input?.action === "string" ? input.action : "show";
129
+ return action !== "verify";
130
+ }
131
+ return WORKSPACE_BRIDGE_COMMANDS.has(command);
132
+ }
133
+ async function ensureWorkspaceOpsBridge(spine) {
134
+ const root = spine.workspaceRootContext.runtimeRoot;
135
+ if (workspaceOpsBridge?.root === root) {
136
+ return { ok: true, dispatch: workspaceOpsBridge.dispatch };
137
+ }
138
+ disposeWorkspaceOpsBridge();
139
+ const opened = await openWorkspaceOpsBridge(root);
140
+ if (!opened.ok) {
141
+ return opened;
142
+ }
143
+ workspaceOpsBridge = { root, close: opened.close, dispatch: opened.dispatch };
144
+ return { ok: true, dispatch: opened.dispatch };
145
+ }
146
+ async function routeSecondNatureCommand(spine, command, input) {
147
+ const wr = spine.workspaceRootContext;
148
+ const useBridge = wr.resolution !== "unknown" && isWorkspaceBridgeCommand(command, input);
149
+ if (useBridge) {
150
+ const bridge = await ensureWorkspaceOpsBridge(spine);
151
+ if (!bridge.ok) {
152
+ return {
153
+ ok: false,
154
+ surfaceMode: "host_safe_carrier",
155
+ workspaceReadModelsEvaluated: false,
156
+ message: HOST_SAFE_LIMITATION_MESSAGE,
157
+ error: bridge.error,
158
+ data: {
159
+ workspaceRootResolution: wr.resolution,
160
+ bridgeAttempted: true,
161
+ declaredRoot: wr.declaredRoot,
162
+ },
163
+ };
164
+ }
165
+ const payload = (await bridge.dispatch(command, input));
166
+ return withSetupNudge(spine, command, payload);
167
+ }
168
+ const def = spine.router.resolve(command);
169
+ if (!def) {
170
+ return { ok: false, message: `Unknown Second Nature command: ${command}` };
171
+ }
172
+ return withSetupNudge(spine, command, await def.execute(input));
173
+ }
174
+ function resolveWorkspaceRoot(toolWorkspaceRoot) {
175
+ const env = process.env.SECOND_NATURE_WORKSPACE_ROOT?.trim();
176
+ if (env) {
177
+ return { resolution: "env", declaredRoot: env, runtimeRoot: env };
178
+ }
179
+ const tool = toolWorkspaceRoot?.trim();
180
+ if (tool) {
181
+ return { resolution: "tool_args", declaredRoot: tool, runtimeRoot: tool };
182
+ }
183
+ return {
184
+ resolution: "unknown",
185
+ declaredRoot: undefined,
186
+ runtimeRoot: process.cwd(),
187
+ };
188
+ }
189
+ function syncWorkspaceRootFromTool(spine, toolWorkspaceRoot) {
190
+ const next = resolveWorkspaceRoot(toolWorkspaceRoot);
191
+ const prev = spine.workspaceRootContext;
192
+ const changed = next.runtimeRoot !== prev.runtimeRoot ||
193
+ next.resolution !== prev.resolution;
194
+ if (changed) {
195
+ disposeWorkspaceOpsBridge();
196
+ }
197
+ spine.workspaceRootContext = next;
198
+ if (changed) {
199
+ spine.runtimeHandle = startRuntimeService({
200
+ workspaceRoot: next.runtimeRoot,
201
+ version: PLUGIN_VERSION,
202
+ });
203
+ }
204
+ }
205
+ function trimRuntimeEvidence(spine) {
206
+ if (spine.runtimeEvidence.length > 12) {
207
+ spine.runtimeEvidence.splice(0, spine.runtimeEvidence.length - 12);
208
+ }
209
+ }
210
+ function latestRuntimeEvidence(spine) {
211
+ return spine.runtimeEvidence[spine.runtimeEvidence.length - 1];
212
+ }
213
+ function createUnavailableActionError(code, message, requiredUserInput, nextStep) {
214
+ return {
215
+ ok: false,
216
+ error: {
217
+ code,
218
+ message,
219
+ requiredUserInput,
220
+ nextStep,
221
+ },
222
+ message: HOST_SAFE_LIMITATION_MESSAGE,
223
+ };
224
+ }
225
+ function getPluginPackageRoot() {
226
+ return path.dirname(fileURLToPath(import.meta.url));
227
+ }
228
+ function safeShortText(value, maxLength = 240) {
229
+ if (typeof value !== "string") {
230
+ return undefined;
231
+ }
232
+ const trimmed = value.trim();
233
+ if (!trimmed) {
234
+ return undefined;
235
+ }
236
+ return trimmed.length > maxLength
237
+ ? `${trimmed.slice(0, maxLength - 3)}...`
238
+ : trimmed;
239
+ }
240
+ function resolveSetupMarkerPath(spine) {
241
+ if (spine.workspaceRootContext.resolution === "unknown") {
242
+ return undefined;
243
+ }
244
+ return path.join(spine.workspaceRootContext.runtimeRoot, SETUP_MARKER_RELATIVE_PATH);
245
+ }
246
+ function readSetupAckMarker(spine) {
247
+ const markerPath = resolveSetupMarkerPath(spine);
248
+ if (!markerPath) {
249
+ return { status: "workspace_root_unknown" };
250
+ }
251
+ if (!fs.existsSync(markerPath)) {
252
+ return { status: "pending", markerPath };
253
+ }
254
+ try {
255
+ const marker = JSON.parse(fs.readFileSync(markerPath, "utf-8"));
256
+ return {
257
+ status: "acknowledged",
258
+ markerPath,
259
+ acknowledgedAt: marker.acknowledgedAt,
260
+ placedIn: marker.placedIn,
261
+ };
262
+ }
263
+ catch {
264
+ return { status: "pending", markerPath };
265
+ }
266
+ }
267
+ function readPackagedSetupText(fileName) {
268
+ const fullPath = path.join(getPluginPackageRoot(), fileName);
269
+ try {
270
+ return {
271
+ ok: true,
272
+ path: fileName,
273
+ content: fs.readFileSync(fullPath, "utf-8"),
274
+ };
275
+ }
276
+ catch (error) {
277
+ return {
278
+ ok: false,
279
+ path: fileName,
280
+ error: error instanceof Error ? error.message : String(error),
281
+ };
282
+ }
283
+ }
284
+ function summarizeSetupText(content) {
285
+ const lines = content
286
+ .split(/\r?\n/)
287
+ .map((line) => line.trim())
288
+ .filter((line) => line && !line.startsWith("#"));
289
+ return lines.slice(0, 6).join("\n");
290
+ }
291
+ function buildSetupNudge(spine) {
292
+ const ack = readSetupAckMarker(spine);
293
+ if (ack.status === "acknowledged") {
294
+ return undefined;
295
+ }
296
+ return {
297
+ status: ack.status,
298
+ command: "setup_hint",
299
+ ackCommand: "setup_ack",
300
+ message: "Second Nature has an unread agent guide. Run setup_hint, read the returned SKILL and guide, place that guidance into the agent's working anchors, then run setup_ack.",
301
+ markerPath: ack.markerPath,
302
+ requiredUserInput: ack.status === "workspace_root_unknown"
303
+ ? ["SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot"]
304
+ : [],
305
+ };
306
+ }
307
+ function withSetupNudge(spine, command, payload) {
308
+ if (SETUP_COMMANDS.has(command) || payload.setupNudge !== undefined) {
309
+ return payload;
310
+ }
311
+ const setupNudge = buildSetupNudge(spine);
312
+ return setupNudge ? { ...payload, setupNudge } : payload;
313
+ }
314
+ function buildSetupHintPayload(spine, input) {
315
+ const format = input?.format === "full" ? "full" : "summary";
316
+ const includeSkill = input?.includeSkill !== false;
317
+ const includeGuide = input?.includeGuide !== false;
318
+ const ack = readSetupAckMarker(spine);
319
+ const data = {
320
+ status: ack.status,
321
+ workspaceRootResolution: spine.workspaceRootContext.resolution,
322
+ markerPath: ack.markerPath,
323
+ acknowledgedAt: ack.acknowledgedAt,
324
+ placedIn: ack.placedIn,
325
+ recommendedPlacement: [
326
+ "agent prompt",
327
+ "workspace/IDENTITY.md",
328
+ "workspace/USER.md",
329
+ ],
330
+ nextStep: ack.status === "acknowledged"
331
+ ? "setup_already_acknowledged"
332
+ : "read_returned_guidance_then_run_setup_ack",
333
+ };
334
+ if (includeSkill) {
335
+ const skill = readPackagedSetupText("SKILL.md");
336
+ data.skill = skill.ok
337
+ ? {
338
+ path: skill.path,
339
+ content: format === "full" ? skill.content : summarizeSetupText(skill.content),
340
+ }
341
+ : skill;
342
+ }
343
+ if (includeGuide) {
344
+ const guide = readPackagedSetupText("agent-inner-guide.md");
345
+ data.guide = guide.ok
346
+ ? {
347
+ path: guide.path,
348
+ content: format === "full" ? guide.content : summarizeSetupText(guide.content),
349
+ }
350
+ : guide;
351
+ }
352
+ return {
353
+ ok: true,
354
+ command: "setup_hint",
355
+ surfaceMode: "host_safe_carrier",
356
+ message: "Read the SKILL and guide as a friendly setup note, then place the guidance where the agent naturally checks its working anchors.",
357
+ data,
358
+ };
359
+ }
360
+ function buildSetupAckPayload(spine, input) {
361
+ const markerPath = resolveSetupMarkerPath(spine);
362
+ if (!markerPath) {
363
+ return {
364
+ ok: false,
365
+ command: "setup_ack",
366
+ error: {
367
+ code: "SETUP_ACK_REQUIRES_WORKSPACE_ROOT",
368
+ message: "setup_ack needs a workspace root so the one-shot marker can be persisted.",
369
+ requiredUserInput: ["SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot"],
370
+ nextStep: "reinvoke_setup_ack_with_workspace_root",
371
+ },
372
+ };
373
+ }
374
+ const marker = {
375
+ acknowledgedAt: new Date().toISOString(),
376
+ acceptedBy: safeShortText(input?.acceptedBy, 80) ?? "agent",
377
+ placedIn: safeShortText(input?.placedIn, 160) ?? "unspecified",
378
+ note: safeShortText(input?.note, 240),
379
+ guideVersion: SETUP_GUIDE_VERSION,
380
+ source: "second-nature-plugin",
381
+ skillPath: "SKILL.md",
382
+ guidePath: "agent-inner-guide.md",
383
+ };
384
+ fs.mkdirSync(path.dirname(markerPath), { recursive: true });
385
+ fs.writeFileSync(markerPath, `${JSON.stringify(marker, null, 2)}\n`, "utf-8");
386
+ return {
387
+ ok: true,
388
+ command: "setup_ack",
389
+ surfaceMode: "host_safe_carrier",
390
+ message: "Setup guide acknowledgement persisted; setup nudge is now silent for this workspace.",
391
+ data: {
392
+ markerPath,
393
+ ...marker,
394
+ },
395
+ };
396
+ }
397
+ function parseExplainSubject(subjectRaw) {
398
+ const trimmed = subjectRaw.trim();
399
+ if (!trimmed) {
400
+ throw new Error("explain_subject_invalid");
401
+ }
402
+ const separatorIndex = trimmed.indexOf(":");
403
+ if (separatorIndex === -1) {
404
+ throw new Error("explain_subject_requires_id");
405
+ }
406
+ const kind = trimmed.slice(0, separatorIndex).trim();
407
+ const id = trimmed.slice(separatorIndex + 1).trim();
408
+ if (!id) {
409
+ throw new Error("explain_subject_requires_id");
410
+ }
411
+ switch (kind) {
412
+ case "decision":
413
+ return { subjectType: "decision", subjectId: id };
414
+ case "platform":
415
+ case "platform-selection":
416
+ return { subjectType: "platform-selection", subjectId: id };
417
+ case "outreach":
418
+ return { subjectType: "outreach", subjectId: id };
419
+ case "soul":
420
+ case "soul-change":
421
+ return { subjectType: "soul-change", subjectId: id };
422
+ case "fallback":
423
+ return { subjectType: "fallback", subjectId: id };
424
+ case "probe":
425
+ return { subjectType: "probe", subjectId: id };
426
+ case "report":
427
+ return { subjectType: "report", subjectId: id };
428
+ case "delivery":
429
+ return { subjectType: "delivery", subjectId: id };
430
+ case "source":
431
+ case "source_ref":
432
+ return { subjectType: "source_ref", subjectId: id };
433
+ default:
434
+ throw new Error("explain_subject_unsupported");
435
+ }
436
+ }
437
+ function buildStatusPayload(spine) {
438
+ const runtimeEvidence = latestRuntimeEvidence(spine);
439
+ const updatedAt = runtimeEvidence?.createdAt ??
440
+ new Date(spine.lifecycleState.lastChangedAt).toISOString();
441
+ const wr = spine.workspaceRootContext;
442
+ const needsRootHint = wr.resolution === "unknown";
443
+ return {
444
+ ok: false,
445
+ surfaceMode: "host_safe_carrier",
446
+ workspaceReadModelsEvaluated: false,
447
+ message: HOST_SAFE_LIMITATION_MESSAGE,
448
+ error: {
449
+ code: "WORKSPACE_READ_SURFACE_UNAVAILABLE",
450
+ message: "Aggregated status requires workspace state; the host-safe plugin does not load persisted read models on this surface.",
451
+ requiredUserInput: needsRootHint
452
+ ? ["SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot"]
453
+ : [],
454
+ nextStep: "run_workspace_second_nature_cli_or_full_runtime_package",
455
+ },
456
+ data: {
457
+ workspaceRootResolution: wr.resolution,
458
+ carrier: {
459
+ host: "openclaw-plugin",
460
+ serviceStatus: spine.runtimeHandle.ready ? "running" : "idle",
461
+ updatedAt,
462
+ lastRuntimeTraceId: runtimeEvidence?.traceId,
463
+ },
464
+ },
465
+ };
466
+ }
467
+ function buildQuietPayload(spine, scope) {
468
+ const wr = spine.workspaceRootContext;
469
+ return {
470
+ ok: false,
471
+ surfaceMode: "host_safe_carrier",
472
+ workspaceReadModelsEvaluated: false,
473
+ message: HOST_SAFE_LIMITATION_MESSAGE,
474
+ error: {
475
+ code: "QUIET_READ_SURFACE_UNAVAILABLE",
476
+ message: "Quiet read surface requires workspace runtime; not evaluated in host-safe carrier mode.",
477
+ requiredUserInput: wr.resolution === "unknown"
478
+ ? ["SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot"]
479
+ : [],
480
+ nextStep: "run_workspace_second_nature_cli_or_full_runtime_package",
481
+ },
482
+ data: {
483
+ scope,
484
+ evaluated: false,
485
+ unavailableReason: "host_safe_carrier_no_workspace_db",
486
+ workspaceRootResolution: wr.resolution,
487
+ },
488
+ };
489
+ }
490
+ function buildReportPayload(spine, day) {
491
+ const wr = spine.workspaceRootContext;
492
+ return {
493
+ ok: false,
494
+ surfaceMode: "host_safe_carrier",
495
+ workspaceReadModelsEvaluated: false,
496
+ message: HOST_SAFE_LIMITATION_MESSAGE,
497
+ error: {
498
+ code: "REPORT_READ_SURFACE_UNAVAILABLE",
499
+ message: "Daily report artifacts require workspace runtime.",
500
+ requiredUserInput: wr.resolution === "unknown"
501
+ ? ["SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot"]
502
+ : [],
503
+ nextStep: "run_workspace_second_nature_cli_or_full_runtime_package",
504
+ },
505
+ data: {
506
+ evaluated: false,
507
+ unavailableReason: "host_safe_carrier_no_workspace_db",
508
+ day: day && day.trim() ? day : new Date().toISOString().slice(0, 10),
509
+ workspaceRootResolution: wr.resolution,
510
+ },
511
+ };
512
+ }
513
+ function buildSessionPayload(spine, sessionId) {
514
+ if (!sessionId) {
515
+ return {
516
+ ok: false,
517
+ error: {
518
+ code: "MISSING_SESSION_ID",
519
+ message: "session show requires sessionId",
520
+ requiredUserInput: ["session_id"],
521
+ nextStep: "reinvoke_session_with_session_id",
522
+ },
523
+ };
524
+ }
525
+ const wr = spine.workspaceRootContext;
526
+ return {
527
+ ok: false,
528
+ surfaceMode: "host_safe_carrier",
529
+ workspaceReadModelsEvaluated: false,
530
+ message: HOST_SAFE_LIMITATION_MESSAGE,
531
+ error: {
532
+ code: "SESSION_READ_SURFACE_UNAVAILABLE",
533
+ message: "Session analytics require workspace state database.",
534
+ requiredUserInput: wr.resolution === "unknown"
535
+ ? ["SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot"]
536
+ : [],
537
+ nextStep: "run_workspace_second_nature_cli_or_full_runtime_package",
538
+ },
539
+ data: {
540
+ requestedSessionId: sessionId,
541
+ evaluated: false,
542
+ unavailableReason: "host_safe_carrier_no_workspace_db",
543
+ workspaceRootResolution: wr.resolution,
544
+ },
545
+ };
546
+ }
547
+ function buildCredentialPayload(spine, platformId) {
548
+ const wr = spine.workspaceRootContext;
549
+ return {
550
+ ok: false,
551
+ surfaceMode: "host_safe_carrier",
552
+ workspaceReadModelsEvaluated: false,
553
+ message: HOST_SAFE_LIMITATION_MESSAGE,
554
+ error: {
555
+ code: "CREDENTIAL_READ_SURFACE_UNAVAILABLE",
556
+ message: "Credential inspection requires workspace runtime on this surface.",
557
+ requiredUserInput: wr.resolution === "unknown"
558
+ ? ["SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot"]
559
+ : [],
560
+ nextStep: "run_workspace_second_nature_cli_or_full_runtime_package",
561
+ },
562
+ data: {
563
+ platformId: platformId && platformId.trim() ? platformId : undefined,
564
+ evaluated: false,
565
+ unavailableReason: "host_safe_carrier_no_workspace_db",
566
+ workspaceRootResolution: wr.resolution,
567
+ },
568
+ };
569
+ }
570
+ function buildExplainPayload(spine, subjectRaw) {
571
+ if (!subjectRaw?.trim()) {
572
+ return {
573
+ ok: false,
574
+ error: {
575
+ code: "MISSING_EXPLAIN_SUBJECT",
576
+ message: "explain requires subject",
577
+ requiredUserInput: ["subject"],
578
+ nextStep: "reinvoke_explain_with_subject",
579
+ },
580
+ };
581
+ }
582
+ let subject;
583
+ try {
584
+ subject = parseExplainSubject(subjectRaw);
585
+ }
586
+ catch (error) {
587
+ const code = error.message;
588
+ if (code === "explain_subject_requires_id") {
589
+ return createUnavailableActionError("EXPLAIN_SUBJECT_REQUIRES_ID", "subject must include identifier", ["subject"], "reinvoke_explain_with_supported_subject");
590
+ }
591
+ if (code === "explain_subject_unsupported") {
592
+ return createUnavailableActionError("EXPLAIN_SUBJECT_UNSUPPORTED", "supported subjects include decision:, platform:, outreach:, soul:, fallback:, delivery:, probe:, report:, source:", ["subject"], "reinvoke_explain_with_supported_subject");
593
+ }
594
+ return createUnavailableActionError("EXPLAIN_SUBJECT_INVALID", "invalid explain subject", ["subject"], "reinvoke_explain_with_supported_subject");
595
+ }
596
+ const wr = spine.workspaceRootContext;
597
+ return {
598
+ ok: false,
599
+ surfaceMode: "host_safe_carrier",
600
+ workspaceReadModelsEvaluated: false,
601
+ message: HOST_SAFE_LIMITATION_MESSAGE,
602
+ error: {
603
+ code: "EXPLAIN_READ_SURFACE_UNAVAILABLE",
604
+ message: "Evidence-backed explain requires persisted workspace read models; host-safe carrier did not evaluate operator explain (CH-11-02).",
605
+ requiredUserInput: wr.resolution === "unknown"
606
+ ? ["SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot"]
607
+ : [],
608
+ nextStep: "run_workspace_second_nature_cli_or_full_runtime_package",
609
+ },
610
+ data: {
611
+ subjectType: subject.subjectType,
612
+ evaluated: false,
613
+ workspaceRootResolution: wr.resolution,
614
+ },
615
+ };
616
+ }
617
+ async function buildStorageSmokePayload(input) {
618
+ try {
619
+ const mod = await import("./runtime/storage/bootstrap/storage-mode-smoke.js");
620
+ const runRepairFixture = Boolean(input?.runRepairFixture);
621
+ const workspaceRoot = typeof input?.workspaceRoot === "string"
622
+ ? input.workspaceRoot
623
+ : undefined;
624
+ const data = await mod.runStorageModeSmoke({
625
+ runRepairFixture,
626
+ workspaceRoot,
627
+ });
628
+ return { ok: true, data };
629
+ }
630
+ catch (error) {
631
+ return {
632
+ ok: false,
633
+ message: error instanceof Error ? error.message : String(error),
634
+ error: {
635
+ code: "STORAGE_SMOKE_LOAD_FAILED",
636
+ message: "Could not load packaged storage-mode smoke module",
637
+ nextStep: "rebuild_plugin_runtime_package",
638
+ },
639
+ };
640
+ }
641
+ }
642
+ function buildFallbackHostSafePayload(ref) {
643
+ if (!ref?.trim()) {
644
+ return {
645
+ ok: false,
646
+ error: {
647
+ code: "MISSING_FALLBACK_REF",
648
+ message: "fallback requires ref (e.g. fallback:…)",
649
+ requiredUserInput: ["ref"],
650
+ nextStep: "reinvoke_with_ref",
651
+ },
652
+ };
653
+ }
654
+ 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");
655
+ }
656
+ function isProbeOnlyInput(input) {
657
+ const v = input?.probeOnly;
658
+ return v === true || v === "true" || v === 1 || v === "1";
659
+ }
660
+ function buildHeartbeatCheckPayload(spine, input) {
661
+ const runtimeEvidence = latestRuntimeEvidence(spine);
662
+ const updatedAt = runtimeEvidence?.createdAt ??
663
+ new Date(spine.lifecycleState.lastChangedAt).toISOString();
664
+ const timestamp = typeof input?.timestamp === "string" && input.timestamp.trim().length > 0
665
+ ? input.timestamp
666
+ : updatedAt;
667
+ const wr = spine.workspaceRootContext;
668
+ if (isProbeOnlyInput(input)) {
669
+ return {
670
+ ok: true,
671
+ status: "heartbeat_ok",
672
+ surfaceMode: "capability_probe",
673
+ reasons: ["probe_only"],
674
+ livedExperienceLoopClaimed: false,
675
+ scope: "rhythm",
676
+ trigger: "heartbeat_bridge",
677
+ message: "Capability probe only on the host-safe carrier surface; does not claim a full lived-experience decision loop.",
678
+ data: {
679
+ workspaceRootResolution: wr.resolution,
680
+ runtime: {
681
+ host: "openclaw-plugin",
682
+ serviceStatus: spine.runtimeHandle.ready ? "running" : "idle",
683
+ updatedAt,
684
+ },
685
+ surface: {
686
+ tool: "second_nature_ops",
687
+ command: "second-nature heartbeat_check",
688
+ },
689
+ bridge: {
690
+ timestamp,
691
+ probeOnly: true,
692
+ sessionContextProvided: typeof input?.sessionContext === "string" &&
693
+ input.sessionContext.trim().length > 0,
694
+ heartbeatChecklistProvided: typeof input?.heartbeatChecklist === "string" &&
695
+ input.heartbeatChecklist.trim().length > 0,
696
+ serviceEntryMode: "capability_probe",
697
+ },
698
+ },
699
+ };
700
+ }
701
+ return {
702
+ ok: true,
703
+ status: "runtime_carrier_only",
704
+ surfaceMode: "host_safe_carrier",
705
+ livedExperienceLoopClaimed: false,
706
+ scope: "rhythm",
707
+ trigger: "heartbeat_bridge",
708
+ reasons: ["runtime_carrier_only", "host_safe_bridge_ack"],
709
+ nextAction: "continue_carrier_surface_only",
710
+ 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.",
711
+ data: {
712
+ workspaceRootResolution: wr.resolution,
713
+ runtime: {
714
+ host: "openclaw-plugin",
715
+ serviceStatus: spine.runtimeHandle.ready ? "running" : "idle",
716
+ updatedAt,
717
+ },
718
+ surface: {
719
+ tool: "second_nature_ops",
720
+ command: "second-nature heartbeat_check",
721
+ },
722
+ bridge: {
723
+ timestamp,
724
+ sessionContextProvided: typeof input?.sessionContext === "string" &&
725
+ input.sessionContext.trim().length > 0,
726
+ heartbeatChecklistProvided: typeof input?.heartbeatChecklist === "string" &&
727
+ input.heartbeatChecklist.trim().length > 0,
728
+ serviceEntryMode: "runtime_carrier_only",
729
+ },
730
+ },
731
+ };
732
+ }
733
+ function createHostSafeRouter(spine) {
734
+ const commands = [
735
+ {
736
+ name: "status",
737
+ description: "Show aggregated Second Nature status",
738
+ execute: async () => buildStatusPayload(spine),
739
+ },
740
+ {
741
+ name: "setup_hint",
742
+ description: "Return the packaged setup SKILL and agent inner guide for first-run onboarding",
743
+ execute: async (input) => buildSetupHintPayload(spine, input),
744
+ },
745
+ {
746
+ name: "setup_ack",
747
+ description: "Persist that the packaged setup guide was read and placed into working anchors",
748
+ execute: async (input) => buildSetupAckPayload(spine, input),
749
+ },
750
+ {
751
+ name: "policy",
752
+ description: "Write or inspect policy state",
753
+ execute: async (input) => {
754
+ const action = typeof input?.action === "string" ? input.action : "show";
755
+ if (action === "set") {
756
+ 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");
757
+ }
758
+ return createUnavailableActionError("HOST_SAFE_POLICY_SHOW_UNAVAILABLE", "Policy read requires workspace state database; host-safe plugin does not load persisted policy rows.", [], "run_workspace_second_nature_cli_or_full_runtime_package");
759
+ },
760
+ },
761
+ {
762
+ name: "credential",
763
+ description: "Inspect or recover credential state",
764
+ execute: async (input) => {
765
+ const action = typeof input?.action === "string" ? input.action : "show";
766
+ if (action === "verify") {
767
+ 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");
768
+ }
769
+ const platformId = typeof input?.platformId === "string" ? input.platformId : undefined;
770
+ return buildCredentialPayload(spine, platformId);
771
+ },
772
+ },
773
+ {
774
+ name: "quiet",
775
+ description: "Inspect Quiet lifecycle state",
776
+ execute: async (input) => {
777
+ const scope = typeof input?.scope === "string" ? input.scope : undefined;
778
+ return buildQuietPayload(spine, scope);
779
+ },
780
+ },
781
+ {
782
+ name: "report",
783
+ description: "Show daily report artifacts",
784
+ execute: async (input) => {
785
+ const day = typeof input?.day === "string" ? input.day : undefined;
786
+ return buildReportPayload(spine, day);
787
+ },
788
+ },
789
+ {
790
+ name: "session",
791
+ description: "Inspect continuity session details",
792
+ execute: async (input) => {
793
+ const sessionId = typeof input?.sessionId === "string" ? input.sessionId : undefined;
794
+ return buildSessionPayload(spine, sessionId);
795
+ },
796
+ },
797
+ {
798
+ name: "audit",
799
+ description: "Inspect audit and evidence views",
800
+ execute: async () => createUnavailableActionError("HOST_SAFE_AUDIT_UNAVAILABLE", "Audit read requires workspace observability database; host-safe plugin does not load persisted audit events.", [], "run_workspace_second_nature_cli_or_full_runtime_package"),
801
+ },
802
+ {
803
+ name: "explain",
804
+ description: "Answer why-question explain requests",
805
+ execute: async (input) => {
806
+ const subject = typeof input?.subject === "string" ? input.subject : undefined;
807
+ return buildExplainPayload(spine, subject);
808
+ },
809
+ },
810
+ {
811
+ name: "heartbeat_check",
812
+ description: "Acknowledge the shipping heartbeat bridge round",
813
+ execute: async (input) => buildHeartbeatCheckPayload(spine, input),
814
+ },
815
+ {
816
+ name: "fallback",
817
+ description: "Operator-visible delivery fallback view (full workspace runtime required)",
818
+ execute: async (input) => {
819
+ const ref = typeof input?.ref === "string" ? input.ref.trim() : undefined;
820
+ return buildFallbackHostSafePayload(ref);
821
+ },
822
+ },
823
+ {
824
+ name: "storage_smoke",
825
+ description: "T4.1.4 storage mode smoke report (sql.js vs native probe)",
826
+ execute: async (input) => buildStorageSmokePayload(input),
827
+ },
828
+ {
829
+ name: "capability_probe",
830
+ description: "Probe host capabilities (workspace runtime required for full report)",
831
+ execute: async () => createUnavailableActionError("HOST_SAFE_CAPABILITY_PROBE_UNAVAILABLE", "Full capability probe requires workspace observability database for persistence; host-safe carrier returns static unknown.", [], "run_workspace_second_nature_cli_or_full_runtime_package"),
832
+ },
833
+ {
834
+ name: "near_real_smoke",
835
+ description: "Run near-real connector smoke (workspace runtime + connectors required)",
836
+ execute: async () => createUnavailableActionError("HOST_SAFE_NEAR_REAL_SMOKE_UNAVAILABLE", "Near-real connector smoke requires workspace state and observability databases; host-safe plugin cannot run connector harness.", [], "run_workspace_second_nature_cli_or_full_runtime_package"),
837
+ },
838
+ // v6 ops surface (CR8-01): host-safe router returns unavailable for workspace-only commands
839
+ {
840
+ name: "narrative",
841
+ description: "Show current NarrativeState (workspace runtime required)",
842
+ execute: async () => createUnavailableActionError("HOST_SAFE_NARRATIVE_UNAVAILABLE", "NarrativeState read requires workspace state database; host-safe plugin does not load persisted narrative rows.", [], "run_workspace_second_nature_cli_or_full_runtime_package"),
843
+ },
844
+ {
845
+ name: "goal",
846
+ description: "Owner-governed goal operations (workspace runtime required)",
847
+ execute: async (input) => {
848
+ const action = typeof input?.action === "string" ? input.action : "list";
849
+ if (action === "set" || action === "accept" || action === "reject") {
850
+ return createUnavailableActionError("HOST_SAFE_GOAL_MUTATE_UNAVAILABLE", "Goal mutation requires workspace state database; host-safe plugin cannot write persisted goal rows.", [], "run_workspace_second_nature_cli_or_full_runtime_package");
851
+ }
852
+ return createUnavailableActionError("HOST_SAFE_GOAL_READ_UNAVAILABLE", "Goal list/read requires workspace state database; host-safe plugin does not load persisted goal rows.", [], "run_workspace_second_nature_cli_or_full_runtime_package");
853
+ },
854
+ },
855
+ {
856
+ name: "dream:recent",
857
+ description: "Show recent Dream runs (workspace runtime required)",
858
+ execute: async () => createUnavailableActionError("HOST_SAFE_DREAM_RECENT_UNAVAILABLE", "Dream recent read requires workspace observability database; host-safe plugin does not load persisted audit events.", [], "run_workspace_second_nature_cli_or_full_runtime_package"),
859
+ },
860
+ {
861
+ name: "connector_status",
862
+ description: "Show connector inventory (workspace runtime required)",
863
+ execute: async () => createUnavailableActionError("HOST_SAFE_CONNECTOR_STATUS_UNAVAILABLE", "Connector status requires workspace state and registry scan; host-safe plugin cannot access connector manifests.", [], "run_workspace_second_nature_cli_or_full_runtime_package"),
864
+ },
865
+ {
866
+ name: "connector_test",
867
+ description: "Dry-run test a connector (workspace runtime required)",
868
+ execute: async () => createUnavailableActionError("HOST_SAFE_CONNECTOR_TEST_UNAVAILABLE", "Connector test requires workspace state and registry; host-safe plugin cannot run connector harness.", [], "run_workspace_second_nature_cli_or_full_runtime_package"),
869
+ },
870
+ {
871
+ name: "connector_behavior_add",
872
+ description: "Add a workspace connector behavior declaration (workspace runtime required)",
873
+ execute: async () => createUnavailableActionError("HOST_SAFE_CONNECTOR_BEHAVIOR_ADD_UNAVAILABLE", "Connector behavior authoring writes workspace manifests; host-safe plugin cannot mutate connector files.", ["platformId", "behaviorId"], "run_workspace_second_nature_cli_or_full_runtime_package"),
874
+ },
875
+ {
876
+ name: "cycle:recent",
877
+ description: "Show recent cycle summary (workspace runtime required)",
878
+ execute: async () => createUnavailableActionError("HOST_SAFE_CYCLE_RECENT_UNAVAILABLE", "Cycle recent read requires workspace observability database; host-safe plugin does not load persisted audit events.", [], "run_workspace_second_nature_cli_or_full_runtime_package"),
879
+ },
880
+ ];
881
+ return {
882
+ commands,
883
+ resolve(name) {
884
+ return commands.find((command) => command.name === name);
885
+ },
886
+ };
887
+ }
888
+ function createActivationSpine() {
889
+ const workspaceRootContext = resolveWorkspaceRoot(undefined);
890
+ const spine = {
891
+ router: undefined,
892
+ runtimeHandle: startRuntimeService({
893
+ workspaceRoot: workspaceRootContext.runtimeRoot,
894
+ version: PLUGIN_VERSION,
895
+ }),
896
+ lifecycleState: getLifecycleState(),
897
+ serviceStartRecorded: false,
898
+ runtimeEvidence: [],
899
+ workspaceRootContext,
900
+ };
901
+ spine.router = createHostSafeRouter(spine);
902
+ return spine;
903
+ }
904
+ function ensureActivationSpine() {
905
+ if (activationSpine) {
906
+ return activationSpine;
907
+ }
908
+ activationSpine = createActivationSpine();
909
+ return activationSpine;
910
+ }
911
+ function recordRuntimeEvidence(spine, origin) {
912
+ if (origin === "service_start" && spine.serviceStartRecorded) {
913
+ return;
914
+ }
915
+ if (origin === "service_start") {
916
+ spine.serviceStartRecorded = true;
917
+ }
918
+ spine.runtimeEvidence.push({
919
+ traceId: `${INTERNAL_RUNTIME_TRACE_PREFIX}${origin}-${spine.lifecycleState.registerCount}-${Date.now()}`,
920
+ capability: origin === "register"
921
+ ? spine.lifecycleState.registerCount === 1
922
+ ? "runtime.activate"
923
+ : "runtime.reload"
924
+ : "runtime.heartbeat",
925
+ origin,
926
+ createdAt: new Date().toISOString(),
927
+ status: "succeeded",
928
+ });
929
+ trimRuntimeEvidence(spine);
930
+ }
931
+ function refreshRegistrationState() {
932
+ const spine = ensureActivationSpine();
933
+ const workspaceRootContext = resolveWorkspaceRoot(undefined);
934
+ const prev = spine.workspaceRootContext;
935
+ const changed = workspaceRootContext.runtimeRoot !== prev.runtimeRoot ||
936
+ workspaceRootContext.resolution !== prev.resolution;
937
+ if (changed) {
938
+ disposeWorkspaceOpsBridge();
939
+ }
940
+ spine.workspaceRootContext = workspaceRootContext;
941
+ spine.runtimeHandle = startRuntimeService({
942
+ workspaceRoot: workspaceRootContext.runtimeRoot,
943
+ version: PLUGIN_VERSION,
944
+ });
945
+ spine.lifecycleState = recordRegistration();
946
+ spine.serviceStartRecorded = false;
947
+ recordRuntimeEvidence(spine, "register");
948
+ return spine;
949
+ }
950
+ function parseCommandInput(rawArgs) {
951
+ const tokens = rawArgs?.trim().split(/\s+/).filter(Boolean) ?? [];
952
+ if (tokens.length === 0) {
953
+ return {
954
+ ok: false,
955
+ result: { ok: false, message: "Missing command argument." },
956
+ };
957
+ }
958
+ const [command, ...rest] = tokens;
959
+ if (command === "policy" && rest[0] === "set") {
960
+ return {
961
+ ok: false,
962
+ result: {
963
+ ok: false,
964
+ command,
965
+ message: "policy set requires structured args; use second_nature_ops instead.",
966
+ },
967
+ };
968
+ }
969
+ if (command === "credential" && rest[0] === "verify") {
970
+ return {
971
+ ok: false,
972
+ result: {
973
+ ok: false,
974
+ command,
975
+ message: "credential verify requires structured args; use second_nature_ops instead.",
976
+ },
977
+ };
978
+ }
979
+ switch (command) {
980
+ case "setup_hint":
981
+ return {
982
+ ok: true,
983
+ command,
984
+ input: rest.includes("--full") ? { format: "full" } : undefined,
985
+ };
986
+ case "setup_ack":
987
+ return {
988
+ ok: true,
989
+ command,
990
+ input: rest.length > 0
991
+ ? { acceptedBy: rest[0], placedIn: rest.slice(1).join(" ") }
992
+ : undefined,
993
+ };
994
+ case "status":
995
+ case "quiet":
996
+ return {
997
+ ok: true,
998
+ command,
999
+ input: rest.length > 0 ? { scope: rest.join(" ") } : undefined,
1000
+ };
1001
+ case "report":
1002
+ return {
1003
+ ok: true,
1004
+ command,
1005
+ input: rest[0] ? { day: rest[0] } : undefined,
1006
+ };
1007
+ case "session":
1008
+ return {
1009
+ ok: true,
1010
+ command,
1011
+ input: rest[0] ? { sessionId: rest[0] } : undefined,
1012
+ };
1013
+ case "credential":
1014
+ return {
1015
+ ok: true,
1016
+ command,
1017
+ input: rest[0] ? { platformId: rest[0] } : undefined,
1018
+ };
1019
+ case "heartbeat_check":
1020
+ return {
1021
+ ok: true,
1022
+ command,
1023
+ input: rest.length > 0
1024
+ ? {
1025
+ timestamp: rest[0],
1026
+ sessionContext: rest.length > 1 ? rest.slice(1).join(" ") : undefined,
1027
+ }
1028
+ : undefined,
1029
+ };
1030
+ case "explain":
1031
+ return {
1032
+ ok: true,
1033
+ command,
1034
+ input: rest.length > 0 ? { subject: rest.join(" ") } : undefined,
1035
+ };
1036
+ case "fallback":
1037
+ return {
1038
+ ok: true,
1039
+ command,
1040
+ input: rest.length > 0 ? { ref: rest.join(" ") } : undefined,
1041
+ };
1042
+ case "storage_smoke": {
1043
+ const wantRepair = rest[0] === "repair" || rest.includes("--repair");
1044
+ return {
1045
+ ok: true,
1046
+ command,
1047
+ input: wantRepair ? { runRepairFixture: true } : undefined,
1048
+ };
1049
+ }
1050
+ // v6 ops surface (CR8-01): simple command parsing for new commands
1051
+ case "narrative":
1052
+ return {
1053
+ ok: true,
1054
+ command,
1055
+ input: rest[0] ? { narrativeId: rest[0] } : undefined,
1056
+ };
1057
+ case "goal":
1058
+ return {
1059
+ ok: true,
1060
+ command,
1061
+ input: rest.length > 0 ? { action: rest[0], goalId: rest[1] } : undefined,
1062
+ };
1063
+ case "dream:recent":
1064
+ return {
1065
+ ok: true,
1066
+ command,
1067
+ input: rest[0] ? { limit: Number(rest[0]) } : undefined,
1068
+ };
1069
+ case "connector_status":
1070
+ return { ok: true, command, input: undefined };
1071
+ case "connector_test":
1072
+ return {
1073
+ ok: true,
1074
+ command,
1075
+ input: rest[0] ? { platformId: rest[0] } : undefined,
1076
+ };
1077
+ case "connector_behavior_add":
1078
+ return {
1079
+ ok: true,
1080
+ command,
1081
+ input: rest.length > 1
1082
+ ? { platformId: rest[0], behaviorId: rest[1], description: rest.slice(2).join(" ") }
1083
+ : undefined,
1084
+ };
1085
+ case "cycle:recent":
1086
+ return {
1087
+ ok: true,
1088
+ command,
1089
+ input: rest[0] ? { limit: Number(rest[0]) } : undefined,
1090
+ };
1091
+ // v7 ops surface (T-ROS.C.2)
1092
+ case "self_health":
1093
+ return { ok: true, command, input: undefined };
1094
+ case "tool_affordance":
1095
+ return {
1096
+ ok: true,
1097
+ command,
1098
+ input: rest[0] ? { query: rest.join(" ") } : undefined,
1099
+ };
1100
+ case "heartbeat_digest":
1101
+ return {
1102
+ ok: true,
1103
+ command,
1104
+ input: rest[0] ? { date: rest[0] } : undefined,
1105
+ };
1106
+ case "snapshot:capture":
1107
+ return {
1108
+ ok: true,
1109
+ command,
1110
+ input: rest[0] ? { snapshotId: rest[0] } : undefined,
1111
+ };
1112
+ case "narrative:diff":
1113
+ return {
1114
+ ok: true,
1115
+ command,
1116
+ input: rest.length >= 2 ? { from: rest[0], to: rest[1] } : undefined,
1117
+ };
1118
+ case "timeline":
1119
+ return {
1120
+ ok: true,
1121
+ command,
1122
+ input: rest[0] ? { limit: Number(rest[0]) } : undefined,
1123
+ };
1124
+ case "restore":
1125
+ return {
1126
+ ok: true,
1127
+ command,
1128
+ // restore <restoreTarget> <fromVersion> <toVersion>
1129
+ input: rest.length >= 3
1130
+ ? { restoreTarget: rest[0], fromVersion: rest[1], toVersion: rest[2] }
1131
+ : undefined,
1132
+ };
1133
+ case "runtime_secret_bootstrap":
1134
+ return { ok: true, command, input: undefined };
1135
+ case "connector:run":
1136
+ return {
1137
+ ok: true,
1138
+ command,
1139
+ // connector:run <platformId> <capabilityId> [payloadJson]
1140
+ input: rest.length >= 2
1141
+ ? {
1142
+ platformId: rest[0],
1143
+ capabilityId: rest[1],
1144
+ payload: rest[2] ? JSON.parse(rest[2]) : undefined,
1145
+ }
1146
+ : undefined,
1147
+ };
1148
+ default:
1149
+ return {
1150
+ ok: true,
1151
+ command,
1152
+ input: undefined,
1153
+ };
1154
+ }
1155
+ }
1156
+ function createRuntimeService() {
1157
+ return {
1158
+ id: "second-nature-runtime",
1159
+ start() {
1160
+ const spine = ensureActivationSpine();
1161
+ recordRuntimeEvidence(spine, "service_start");
1162
+ return {
1163
+ ready: spine.runtimeHandle.ready,
1164
+ version: spine.runtimeHandle.version,
1165
+ };
1166
+ },
1167
+ };
1168
+ }
1169
+ function createLifecycleService() {
1170
+ return {
1171
+ id: "second-nature-lifecycle",
1172
+ start() {
1173
+ const spine = ensureActivationSpine();
1174
+ return {
1175
+ phase: spine.lifecycleState.phase,
1176
+ registerCount: spine.lifecycleState.registerCount,
1177
+ lastChangedAt: spine.lifecycleState.lastChangedAt,
1178
+ };
1179
+ },
1180
+ };
1181
+ }
1182
+ const SECOND_NATURE_TOOL_SCHEMA = {
1183
+ type: "object",
1184
+ additionalProperties: false,
1185
+ properties: {
1186
+ command: { type: "string" },
1187
+ args: { type: "object", additionalProperties: true },
1188
+ workspaceRoot: {
1189
+ type: "string",
1190
+ description: "Workspace root for packaged smoke/runtime alignment (optional; prefer SECOND_NATURE_WORKSPACE_ROOT).",
1191
+ },
1192
+ },
1193
+ required: ["command"],
1194
+ };
1195
+ export default {
1196
+ id: "second-nature",
1197
+ name: "Second Nature",
1198
+ description: "Registers command/tool/service surface with load-reload lifecycle semantics.",
1199
+ register(api) {
1200
+ process.stderr.write(`[second-nature] register() entered, api keys=${Object.keys(api).join(",")}\n`);
1201
+ const runtimeService = createRuntimeService();
1202
+ const lifecycleService = createLifecycleService();
1203
+ api.registerService(runtimeService);
1204
+ api.registerService(lifecycleService);
1205
+ api.registerCommand({
1206
+ name: "second-nature",
1207
+ description: "Route Agent-facing operational commands for Second Nature.",
1208
+ acceptsArgs: true,
1209
+ handler: async (ctx) => {
1210
+ const spine = ensureActivationSpine();
1211
+ const parsed = parseCommandInput(ctx.args);
1212
+ if (!parsed.ok) {
1213
+ return {
1214
+ text: JSON.stringify(parsed.result),
1215
+ };
1216
+ }
1217
+ const resolved = spine.router.resolve(parsed.command);
1218
+ if (!resolved && !isWorkspaceBridgeCommand(parsed.command, parsed.input)) {
1219
+ return {
1220
+ text: JSON.stringify({
1221
+ ok: false,
1222
+ command: parsed.command,
1223
+ message: "Unknown Second Nature command.",
1224
+ }),
1225
+ };
1226
+ }
1227
+ const result = await routeSecondNatureCommand(spine, parsed.command, parsed.input);
1228
+ return {
1229
+ text: JSON.stringify(result),
1230
+ };
1231
+ },
1232
+ });
1233
+ const executeSecondNatureTool = async (params) => {
1234
+ const spine = ensureActivationSpine();
1235
+ syncWorkspaceRootFromTool(spine, params.workspaceRoot);
1236
+ const resolved = spine.router.resolve(params.command);
1237
+ if (!resolved && !isWorkspaceBridgeCommand(params.command, params.args)) {
1238
+ return {
1239
+ content: [
1240
+ {
1241
+ type: "text",
1242
+ text: JSON.stringify({
1243
+ ok: false,
1244
+ message: "Unknown Second Nature command.",
1245
+ }),
1246
+ },
1247
+ ],
1248
+ };
1249
+ }
1250
+ const result = await routeSecondNatureCommand(spine, params.command, params.args);
1251
+ return {
1252
+ content: [
1253
+ {
1254
+ type: "text",
1255
+ text: JSON.stringify(result),
1256
+ },
1257
+ ],
1258
+ };
1259
+ };
1260
+ api.registerTool({
1261
+ name: "second_nature_ops",
1262
+ description: "Access the Second Nature command surface through a single tool shell.",
1263
+ parameters: SECOND_NATURE_TOOL_SCHEMA,
1264
+ async execute(_id, params) {
1265
+ return executeSecondNatureTool(params);
1266
+ },
1267
+ });
1268
+ process.stderr.write("[second-nature] register() completed\n");
1269
+ },
1270
+ };