@haaaiawd/second-nature 0.2.9 → 0.2.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +102 -6
- package/openclaw.plugin.json +2 -5
- 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/host-capability/probe-host-capability.js +1 -1
- package/runtime/cli/host-capability/types.d.ts +2 -7
- package/runtime/cli/host-capability/types.js +0 -5
- package/runtime/cli/ops/heartbeat-surface.d.ts +5 -3
- package/runtime/cli/ops/heartbeat-surface.js +38 -8
- package/runtime/cli/ops/ops-router.d.ts +6 -2
- package/runtime/cli/ops/ops-router.js +1275 -1147
- package/runtime/cli/ops/workspace-heartbeat-runner.js +2 -5
- 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/connectors/manifest/manifest-schema.d.ts +2 -2
- package/runtime/connectors/registry/dynamic-connector-registry.js +16 -1
- package/runtime/connectors/services/connector-executor-adapter.js +54 -35
- package/runtime/core/second-nature/action/action-closure-recorder.d.ts +4 -0
- package/runtime/core/second-nature/action/action-closure-recorder.js +51 -38
- package/runtime/core/second-nature/action/action-proposal-builder.js +8 -34
- package/runtime/core/second-nature/action/policy-bound-dispatch.d.ts +2 -0
- package/runtime/core/second-nature/action/policy-bound-dispatch.js +10 -5
- package/runtime/core/second-nature/control-plane/accepted-projection-loader.js +1 -11
- 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.d.ts +1 -0
- package/runtime/core/second-nature/control-plane/heartbeat-orchestrator.js +23 -15
- package/runtime/core/second-nature/control-plane/real-runtime-spine.d.ts +2 -0
- package/runtime/core/second-nature/control-plane/real-runtime-spine.js +2 -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/heartbeat/heartbeat-loop.js +4 -3
- package/runtime/core/second-nature/heartbeat/runtime-snapshot.d.ts +3 -2
- package/runtime/core/second-nature/heartbeat/snapshot-builder.d.ts +3 -2
- package/runtime/core/second-nature/orchestrator/intent-planner.js +4 -2
- package/runtime/core/second-nature/orchestrator/narrative-update.js +1 -2
- package/runtime/core/second-nature/orchestrator/platform-capability-router.d.ts +2 -2
- package/runtime/core/second-nature/orchestrator/platform-capability-router.js +1 -1
- package/runtime/core/second-nature/outreach/build-outreach-draft-request.js +2 -3
- package/runtime/core/second-nature/outreach/dispatch-user-outreach.d.ts +2 -2
- package/runtime/core/second-nature/outreach/dispatch-user-outreach.js +6 -1
- package/runtime/core/second-nature/outreach/judge-input-from-snapshot.js +3 -14
- package/runtime/core/second-nature/outreach/judge-outreach.d.ts +6 -5
- package/runtime/core/second-nature/perception/judgment-engine.js +10 -16
- package/runtime/core/second-nature/perception/perception-builder.js +15 -11
- package/runtime/core/second-nature/quiet/run-source-backed-quiet.js +13 -15
- package/runtime/core/second-nature/quiet-dream/daily-rhythm-scheduler.js +40 -16
- 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 -3
- package/runtime/core/second-nature/quiet-dream/memory-projection-lifecycle.js +2 -13
- 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 +34 -11
- package/runtime/core/second-nature/types.d.ts +2 -9
- package/runtime/dream/dream-engine.js +11 -4
- package/runtime/guidance/outreach-draft-schema.d.ts +12 -12
- package/runtime/guidance/persona-selection.js +5 -0
- package/runtime/guidance/template-registry.js +6 -1
- package/runtime/guidance/types.d.ts +2 -2
- package/runtime/observability/causal-loop-health.d.ts +2 -1
- package/runtime/observability/causal-loop-health.js +7 -0
- package/runtime/observability/living-loop-health-gate.js +2 -8
- package/runtime/observability/loop-stage-event-sink.js +6 -2
- 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/observability/services/lived-experience-audit.d.ts +7 -7
- 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/serialization.d.ts +17 -0
- package/runtime/shared/serialization.js +27 -0
- package/runtime/shared/setup-ack.d.ts +54 -0
- package/runtime/shared/setup-ack.js +108 -0
- package/runtime/shared/source-ref-compat.d.ts +26 -0
- package/runtime/shared/source-ref-compat.js +64 -0
- package/runtime/shared/types/goal.d.ts +4 -4
- package/runtime/shared/types/goal.js +1 -1
- package/runtime/shared/types/index.d.ts +1 -0
- package/runtime/shared/types/index.js +1 -3
- package/runtime/shared/types/source-ref.d.ts +2 -2
- package/runtime/shared/types/v7-entities.d.ts +5 -5
- package/runtime/shared/types/v7-entities.js +1 -1
- package/runtime/shared/types/v8-contracts.d.ts +13 -2
- package/runtime/storage/db/index.js +60 -12
- package/runtime/storage/db/migrations/index.js +4 -0
- package/runtime/storage/db/migrations/v8-004-schema-closure.d.ts +19 -0
- package/runtime/storage/db/migrations/v8-004-schema-closure.js +74 -0
- package/runtime/storage/db/migrations/v8-005-single-status-schema.d.ts +11 -0
- package/runtime/storage/db/migrations/v8-005-single-status-schema.js +16 -0
- 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 +65 -84
- package/runtime/storage/db/schema/v8-entities.js +8 -7
- package/runtime/storage/delivery/types.d.ts +2 -2
- package/runtime/storage/fallback/load-operator-fallback.d.ts +2 -2
- package/runtime/storage/fallback/operator-fallback-types.d.ts +2 -2
- package/runtime/storage/fallback/operator-fallback-view.d.ts +2 -2
- package/runtime/storage/index.d.ts +1 -1
- package/runtime/storage/life-evidence/types.d.ts +5 -5
- package/runtime/storage/quiet/quiet-artifact-types.d.ts +4 -4
- package/runtime/storage/quiet/quiet-artifact-writer.d.ts +2 -2
- package/runtime/storage/services/write-validation-gate.d.ts +1 -1
- package/runtime/storage/services/write-validation-gate.js +15 -3
- package/runtime/storage/snapshots/types.d.ts +8 -8
- package/runtime/storage/user-interest/types.d.ts +3 -3
- package/runtime/storage/v8-state-stores.d.ts +15 -3
- package/runtime/storage/v8-state-stores.js +60 -39
package/index.js
CHANGED
|
@@ -31,6 +31,12 @@
|
|
|
31
31
|
* illusion of a working plugin while agent sessions see no tool. We hit
|
|
32
32
|
* exactly that on 2026-05-06; the fix lives in plugin/openclaw.plugin.json
|
|
33
33
|
* under the `activation` block.
|
|
34
|
+
* - Cloud Feishu/OpenClaw sessions can report `capabilities=none`. In that
|
|
35
|
+
* mode, binding activation to `onCapabilities:["tool"]` prevents the agent
|
|
36
|
+
* tool from entering the session even though the plugin loaded. Therefore
|
|
37
|
+
* the manifest keeps `activation.onStartup === true` for daemon loading and
|
|
38
|
+
* declares the tool through `contracts.tools`, but does not require a session
|
|
39
|
+
* capability to activate `second_nature_ops`.
|
|
34
40
|
* - `Shape: non-capability` reported by `openclaw plugins info` is EXPECTED
|
|
35
41
|
* for this plugin. OpenClaw counts capabilities only across cli-backend /
|
|
36
42
|
* text-inference / speech / realtime-* / media-understanding /
|
|
@@ -45,7 +51,7 @@
|
|
|
45
51
|
* **same absolute path** as the OpenClaw **agent workspace** (default `~/.openclaw/workspace`, or
|
|
46
52
|
* `agents.defaults.workspace` in `~/.openclaw/openclaw.json`). Do **not** infer that root from the plugin
|
|
47
53
|
* install directory. With **sandbox** or **per-agent workspaces**, use the path where `data/state.db` and
|
|
48
|
-
* `workspace/` anchors actually live.
|
|
54
|
+
* `workspace/` anchors actually live.
|
|
49
55
|
*
|
|
50
56
|
* Test coverage:
|
|
51
57
|
* - tests/integration/cli/plugin-runtime-registration.test.ts
|
|
@@ -75,6 +81,20 @@ const HOST_SAFE_LIMITATION_MESSAGE = "Host-safe plugin package keeps synchronous
|
|
|
75
81
|
const SETUP_MARKER_RELATIVE_PATH = path.join(".second-nature", "setup", "agent-inner-guide-ack.json");
|
|
76
82
|
const SETUP_GUIDE_VERSION = "0.1.38";
|
|
77
83
|
const SETUP_COMMANDS = new Set(["setup_hint", "setup_ack"]);
|
|
84
|
+
// T-SH.R.8: Import shared setup-ack validator instead of duplicating.
|
|
85
|
+
// Source of truth: src/shared/setup-ack.ts (copied to plugin/runtime/shared/ by build).
|
|
86
|
+
import { validateSetupAck as validateSetupAckShared, SETUP_ACK_SCHEMA_VERSION, } from "./runtime/shared/setup-ack.js";
|
|
87
|
+
/**
|
|
88
|
+
* Wrapper that adapts the shared validator's richer return type
|
|
89
|
+
* ({ok:true, ack} | {ok:false, errors}) to the plugin's historical
|
|
90
|
+
* ({ok:true} | {ok:false, errors}) shape. Keeps call sites unchanged.
|
|
91
|
+
*/
|
|
92
|
+
function validateSetupAck(raw) {
|
|
93
|
+
const result = validateSetupAckShared(raw);
|
|
94
|
+
if (result.ok)
|
|
95
|
+
return { ok: true };
|
|
96
|
+
return { ok: false, errors: result.errors };
|
|
97
|
+
}
|
|
78
98
|
let activationSpine = null;
|
|
79
99
|
/** T1.1.4 — lazily opened full read bridge; closed when workspace root / resolution changes. */
|
|
80
100
|
let workspaceOpsBridge = null;
|
|
@@ -242,6 +262,31 @@ function safeShortText(value, maxLength = 240) {
|
|
|
242
262
|
? `${trimmed.slice(0, maxLength - 3)}...`
|
|
243
263
|
: trimmed;
|
|
244
264
|
}
|
|
265
|
+
function buildHostDiscoveryReport(input) {
|
|
266
|
+
const observedAt = new Date().toISOString();
|
|
267
|
+
const hostName = typeof input?.hostName === "string" ? input.hostName : undefined;
|
|
268
|
+
const hostVersion = typeof input?.hostVersion === "string" ? input.hostVersion : undefined;
|
|
269
|
+
return {
|
|
270
|
+
toolDiscovery: {
|
|
271
|
+
status: "unsupported",
|
|
272
|
+
tools: [],
|
|
273
|
+
hostName,
|
|
274
|
+
hostVersion,
|
|
275
|
+
observedAt,
|
|
276
|
+
reason: "host_probe_unsupported",
|
|
277
|
+
},
|
|
278
|
+
skillDiscovery: {
|
|
279
|
+
status: "unsupported",
|
|
280
|
+
skills: [],
|
|
281
|
+
observedAt,
|
|
282
|
+
reason: "skill_probe_unsupported",
|
|
283
|
+
},
|
|
284
|
+
setupComplete: false,
|
|
285
|
+
evidenceLevel: "carrier_ack",
|
|
286
|
+
reason: "host_probe_unsupported",
|
|
287
|
+
nextStep: "host_safe_carrier_cannot_probe_host_registry_run_workspace_cli_for_full_discovery",
|
|
288
|
+
};
|
|
289
|
+
}
|
|
245
290
|
function resolveSetupMarkerPath(spine) {
|
|
246
291
|
if (spine.workspaceRootContext.resolution === "unknown") {
|
|
247
292
|
return undefined;
|
|
@@ -258,6 +303,16 @@ function readSetupAckMarker(spine) {
|
|
|
258
303
|
}
|
|
259
304
|
try {
|
|
260
305
|
const marker = JSON.parse(fs.readFileSync(markerPath, "utf-8"));
|
|
306
|
+
const validation = validateSetupAck(marker);
|
|
307
|
+
if (!validation.ok) {
|
|
308
|
+
return {
|
|
309
|
+
status: "incomplete",
|
|
310
|
+
markerPath,
|
|
311
|
+
acknowledgedAt: marker.acknowledgedAt,
|
|
312
|
+
placedIn: marker.placedIn,
|
|
313
|
+
incompleteReasons: validation.errors,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
261
316
|
return {
|
|
262
317
|
status: "acknowledged",
|
|
263
318
|
markerPath,
|
|
@@ -266,7 +321,17 @@ function readSetupAckMarker(spine) {
|
|
|
266
321
|
};
|
|
267
322
|
}
|
|
268
323
|
catch {
|
|
269
|
-
return {
|
|
324
|
+
return {
|
|
325
|
+
status: "incomplete",
|
|
326
|
+
markerPath,
|
|
327
|
+
incompleteReasons: [
|
|
328
|
+
{
|
|
329
|
+
field: "marker",
|
|
330
|
+
reason: "Marker file is not valid JSON",
|
|
331
|
+
repairAction: "Re-run setup_ack to rewrite the marker with a valid schema.",
|
|
332
|
+
},
|
|
333
|
+
],
|
|
334
|
+
};
|
|
270
335
|
}
|
|
271
336
|
}
|
|
272
337
|
function readPackagedSetupText(fileName) {
|
|
@@ -321,20 +386,27 @@ function buildSetupHintPayload(spine, input) {
|
|
|
321
386
|
const includeSkill = input?.includeSkill !== false;
|
|
322
387
|
const includeGuide = input?.includeGuide !== false;
|
|
323
388
|
const ack = readSetupAckMarker(spine);
|
|
389
|
+
const hostDiscovery = buildHostDiscoveryReport(input);
|
|
324
390
|
const data = {
|
|
325
391
|
status: ack.status,
|
|
326
392
|
workspaceRootResolution: spine.workspaceRootContext.resolution,
|
|
327
393
|
markerPath: ack.markerPath,
|
|
328
394
|
acknowledgedAt: ack.acknowledgedAt,
|
|
329
395
|
placedIn: ack.placedIn,
|
|
396
|
+
hostDiscovery,
|
|
330
397
|
recommendedPlacement: [
|
|
331
398
|
"agent prompt",
|
|
332
399
|
"workspace/IDENTITY.md",
|
|
333
400
|
"workspace/USER.md",
|
|
334
401
|
],
|
|
335
402
|
nextStep: ack.status === "acknowledged"
|
|
336
|
-
?
|
|
337
|
-
|
|
403
|
+
? hostDiscovery.setupComplete
|
|
404
|
+
? "setup_verified_by_host_discovery"
|
|
405
|
+
: hostDiscovery.nextStep
|
|
406
|
+
: ack.status === "incomplete"
|
|
407
|
+
? "repair_setup_ack_fields"
|
|
408
|
+
: "read_returned_guidance_then_run_setup_ack",
|
|
409
|
+
...(ack.incompleteReasons ? { incompleteReasons: ack.incompleteReasons } : {}),
|
|
338
410
|
};
|
|
339
411
|
if (includeSkill) {
|
|
340
412
|
const skill = readPackagedSetupText("SKILL.md");
|
|
@@ -358,6 +430,7 @@ function buildSetupHintPayload(spine, input) {
|
|
|
358
430
|
ok: true,
|
|
359
431
|
command: "setup_hint",
|
|
360
432
|
surfaceMode: "host_safe_carrier",
|
|
433
|
+
evidenceLevel: "contract_smoke",
|
|
361
434
|
message: "Read the SKILL and guide as a friendly setup note, then place the guidance where the agent naturally checks its working anchors.",
|
|
362
435
|
data,
|
|
363
436
|
};
|
|
@@ -376,25 +449,48 @@ function buildSetupAckPayload(spine, input) {
|
|
|
376
449
|
},
|
|
377
450
|
};
|
|
378
451
|
}
|
|
452
|
+
const placedIn = safeShortText(input?.placedIn, 160);
|
|
453
|
+
const placementProofRef = safeShortText(input?.placementProofRef, 320);
|
|
454
|
+
const writer = safeShortText(input?.writer, 80);
|
|
379
455
|
const marker = {
|
|
456
|
+
schemaVersion: SETUP_ACK_SCHEMA_VERSION,
|
|
380
457
|
acknowledgedAt: new Date().toISOString(),
|
|
381
458
|
acceptedBy: safeShortText(input?.acceptedBy, 80) ?? "agent",
|
|
382
|
-
placedIn:
|
|
459
|
+
placedIn: placedIn ?? "unspecified",
|
|
460
|
+
placementProofRef: placementProofRef ?? "",
|
|
383
461
|
note: safeShortText(input?.note, 240),
|
|
384
462
|
guideVersion: SETUP_GUIDE_VERSION,
|
|
385
463
|
source: "second-nature-plugin",
|
|
386
464
|
skillPath: "SKILL.md",
|
|
387
465
|
guidePath: "agent-inner-guide.md",
|
|
466
|
+
writer: writer ?? "setup_ack_command",
|
|
388
467
|
};
|
|
468
|
+
const validation = validateSetupAck(marker);
|
|
469
|
+
if (!validation.ok) {
|
|
470
|
+
return {
|
|
471
|
+
ok: false,
|
|
472
|
+
command: "setup_ack",
|
|
473
|
+
surfaceMode: "host_safe_carrier",
|
|
474
|
+
evidenceLevel: "carrier_ack",
|
|
475
|
+
message: "Setup acknowledgement is incomplete; see incompleteReasons and repairAction.",
|
|
476
|
+
data: {
|
|
477
|
+
markerPath,
|
|
478
|
+
incompleteReasons: validation.errors,
|
|
479
|
+
},
|
|
480
|
+
};
|
|
481
|
+
}
|
|
389
482
|
fs.mkdirSync(path.dirname(markerPath), { recursive: true });
|
|
390
483
|
fs.writeFileSync(markerPath, `${JSON.stringify(marker, null, 2)}\n`, "utf-8");
|
|
484
|
+
const hostDiscovery = buildHostDiscoveryReport(input);
|
|
391
485
|
return {
|
|
392
486
|
ok: true,
|
|
393
487
|
command: "setup_ack",
|
|
394
488
|
surfaceMode: "host_safe_carrier",
|
|
395
|
-
|
|
489
|
+
evidenceLevel: hostDiscovery.evidenceLevel,
|
|
490
|
+
message: "Setup guide acknowledgement persisted; setup nudge is now silent for this workspace. Host skill discovery is unavailable in carrier mode.",
|
|
396
491
|
data: {
|
|
397
492
|
markerPath,
|
|
493
|
+
hostDiscovery,
|
|
398
494
|
...marker,
|
|
399
495
|
},
|
|
400
496
|
};
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
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
|
-
"onStartup": true
|
|
8
|
-
"onCapabilities": [
|
|
9
|
-
"tool"
|
|
10
|
-
]
|
|
7
|
+
"onStartup": true
|
|
11
8
|
},
|
|
12
9
|
"contracts": {
|
|
13
10
|
"commands": [
|
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
|
+
}
|
|
@@ -3,15 +3,10 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Test coverage: tests/unit/cli/host-capability.test.ts, tests/integration/cli/host-capability-probe.test.ts
|
|
5
5
|
*/
|
|
6
|
+
import type { SourceRef } from "../../shared/types/v8-contracts.js";
|
|
7
|
+
export type { SourceRef } from "../../shared/types/v8-contracts.js";
|
|
6
8
|
export type DeliveryCapabilityStatus = "target_available" | "target_none" | "channel_missing" | "host_api_unavailable" | "host_unsupported" | "unknown";
|
|
7
9
|
export type CapabilityVerdict = "pass" | "fail" | "unknown" | "not_applicable";
|
|
8
|
-
export interface SourceRef {
|
|
9
|
-
id: string;
|
|
10
|
-
kind: "platform_item" | "workspace_artifact" | "decision_record" | "user_anchor" | "connector_result" | "host_report" | "fallback_artifact";
|
|
11
|
-
uri: string;
|
|
12
|
-
excerptHash?: string;
|
|
13
|
-
observedAt?: string;
|
|
14
|
-
}
|
|
15
10
|
export interface HostCapabilityDocReference {
|
|
16
11
|
title: string;
|
|
17
12
|
url: string;
|