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