@haaaiawd/second-nature 0.1.24 → 0.1.26
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 +78 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +5 -5
- package/runtime/cli/commands/goal.d.ts +28 -0
- package/runtime/cli/commands/goal.js +163 -0
- package/runtime/cli/commands/index.js +38 -3
- package/runtime/cli/explain/resolve-subject.js +3 -0
- package/runtime/cli/ops/ops-router.d.ts +1 -1
- package/runtime/cli/ops/ops-router.js +63 -1
- package/runtime/cli/ops/workspace-heartbeat-runner.d.ts +6 -0
- package/runtime/cli/ops/workspace-heartbeat-runner.js +35 -1
- package/runtime/cli/read-models/index.d.ts +14 -2
- package/runtime/cli/read-models/index.js +403 -101
- package/runtime/cli/read-models/types.d.ts +90 -3
- package/runtime/core/second-nature/feedback/owner-reply-feedback.d.ts +46 -0
- package/runtime/core/second-nature/feedback/owner-reply-feedback.js +159 -0
- package/runtime/core/second-nature/heartbeat/heartbeat-loop.d.ts +11 -1
- package/runtime/core/second-nature/heartbeat/heartbeat-loop.js +78 -10
- package/runtime/core/second-nature/heartbeat/runtime-snapshot.d.ts +2 -0
- package/runtime/core/second-nature/heartbeat/runtime-snapshot.js +1 -1
- package/runtime/core/second-nature/heartbeat/snapshot-builder.d.ts +16 -2
- package/runtime/core/second-nature/index.d.ts +1 -0
- package/runtime/core/second-nature/index.js +1 -0
- package/runtime/core/second-nature/orchestrator/goal-priority.d.ts +16 -3
- package/runtime/core/second-nature/orchestrator/goal-priority.js +10 -9
- package/runtime/core/second-nature/orchestrator/intent-planner.d.ts +29 -1
- package/runtime/core/second-nature/orchestrator/intent-planner.js +154 -79
- package/runtime/core/second-nature/orchestrator/narrative-update.js +23 -9
- package/runtime/core/second-nature/orchestrator/platform-capability-router.d.ts +34 -0
- package/runtime/core/second-nature/orchestrator/platform-capability-router.js +115 -0
- package/runtime/core/second-nature/outreach/build-outreach-draft-request.d.ts +3 -1
- package/runtime/core/second-nature/outreach/build-outreach-draft-request.js +39 -1
- package/runtime/core/second-nature/outreach/dispatch-user-outreach.js +21 -2
- package/runtime/guidance/draft-outreach-message.js +14 -1
- package/runtime/guidance/outreach-draft-schema.d.ts +104 -0
- package/runtime/guidance/outreach-draft-schema.js +14 -0
- package/runtime/observability/audit/audit-envelope.d.ts +1 -1
- package/runtime/observability/query/explain-query.d.ts +3 -0
- package/runtime/observability/query/explain-query.js +9 -0
- package/runtime/observability/services/lived-experience-audit.d.ts +22 -0
- package/runtime/observability/services/lived-experience-audit.js +30 -0
- package/runtime/shared/types/credential.d.ts +1 -1
- package/runtime/storage/chronicle/session-chronicle-store.d.ts +1 -1
- package/runtime/storage/db/schema/narrative-state.d.ts +1 -1
- package/runtime/storage/db/schema/narrative-state.js +2 -2
- package/runtime/storage/services/credential-vault.d.ts +18 -0
- package/runtime/storage/services/credential-vault.js +73 -3
package/index.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* runtime graph currently contains async sql.js bootstrap that breaks vm sandbox loading
|
|
8
8
|
* - expose a minimal in-memory activation spine so status/lifecycle stay truthful even when
|
|
9
9
|
* the full workspace runtime is not loaded inside the host
|
|
10
|
+
* - T4.2.1: owner reply ingestion → RelationshipMemory feedback (full runtime only)
|
|
10
11
|
*
|
|
11
12
|
* Dependencies:
|
|
12
13
|
* - only imports runtime lifecycle/service modules that are synchronous at load time
|
|
@@ -103,6 +104,13 @@ const WORKSPACE_BRIDGE_COMMANDS = new Set([
|
|
|
103
104
|
"audit",
|
|
104
105
|
// T3.3.2: near-real connector smoke sentinel
|
|
105
106
|
"near_real_smoke",
|
|
107
|
+
// v6 ops surface (CR8-01): narrative, goal, dream:recent, connector_status/test, cycle:recent
|
|
108
|
+
"narrative",
|
|
109
|
+
"goal",
|
|
110
|
+
"dream:recent",
|
|
111
|
+
"connector_status",
|
|
112
|
+
"connector_test",
|
|
113
|
+
"cycle:recent",
|
|
106
114
|
]);
|
|
107
115
|
function isWorkspaceBridgeCommand(command, input) {
|
|
108
116
|
if (command === "credential") {
|
|
@@ -632,6 +640,43 @@ function createHostSafeRouter(spine) {
|
|
|
632
640
|
description: "Run near-real connector smoke (workspace runtime + connectors required)",
|
|
633
641
|
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"),
|
|
634
642
|
},
|
|
643
|
+
// v6 ops surface (CR8-01): host-safe router returns unavailable for workspace-only commands
|
|
644
|
+
{
|
|
645
|
+
name: "narrative",
|
|
646
|
+
description: "Show current NarrativeState (workspace runtime required)",
|
|
647
|
+
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"),
|
|
648
|
+
},
|
|
649
|
+
{
|
|
650
|
+
name: "goal",
|
|
651
|
+
description: "Owner-governed goal operations (workspace runtime required)",
|
|
652
|
+
execute: async (input) => {
|
|
653
|
+
const action = typeof input?.action === "string" ? input.action : "list";
|
|
654
|
+
if (action === "set" || action === "accept" || action === "reject") {
|
|
655
|
+
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");
|
|
656
|
+
}
|
|
657
|
+
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");
|
|
658
|
+
},
|
|
659
|
+
},
|
|
660
|
+
{
|
|
661
|
+
name: "dream:recent",
|
|
662
|
+
description: "Show recent Dream runs (workspace runtime required)",
|
|
663
|
+
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"),
|
|
664
|
+
},
|
|
665
|
+
{
|
|
666
|
+
name: "connector_status",
|
|
667
|
+
description: "Show connector inventory (workspace runtime required)",
|
|
668
|
+
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"),
|
|
669
|
+
},
|
|
670
|
+
{
|
|
671
|
+
name: "connector_test",
|
|
672
|
+
description: "Dry-run test a connector (workspace runtime required)",
|
|
673
|
+
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"),
|
|
674
|
+
},
|
|
675
|
+
{
|
|
676
|
+
name: "cycle:recent",
|
|
677
|
+
description: "Show recent cycle summary (workspace runtime required)",
|
|
678
|
+
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"),
|
|
679
|
+
},
|
|
635
680
|
];
|
|
636
681
|
return {
|
|
637
682
|
commands,
|
|
@@ -786,6 +831,39 @@ function parseCommandInput(rawArgs) {
|
|
|
786
831
|
input: wantRepair ? { runRepairFixture: true } : undefined,
|
|
787
832
|
};
|
|
788
833
|
}
|
|
834
|
+
// v6 ops surface (CR8-01): simple command parsing for new commands
|
|
835
|
+
case "narrative":
|
|
836
|
+
return {
|
|
837
|
+
ok: true,
|
|
838
|
+
command,
|
|
839
|
+
input: rest[0] ? { narrativeId: rest[0] } : undefined,
|
|
840
|
+
};
|
|
841
|
+
case "goal":
|
|
842
|
+
return {
|
|
843
|
+
ok: true,
|
|
844
|
+
command,
|
|
845
|
+
input: rest.length > 0 ? { action: rest[0], goalId: rest[1] } : undefined,
|
|
846
|
+
};
|
|
847
|
+
case "dream:recent":
|
|
848
|
+
return {
|
|
849
|
+
ok: true,
|
|
850
|
+
command,
|
|
851
|
+
input: rest[0] ? { limit: Number(rest[0]) } : undefined,
|
|
852
|
+
};
|
|
853
|
+
case "connector_status":
|
|
854
|
+
return { ok: true, command, input: undefined };
|
|
855
|
+
case "connector_test":
|
|
856
|
+
return {
|
|
857
|
+
ok: true,
|
|
858
|
+
command,
|
|
859
|
+
input: rest[0] ? { platformId: rest[0] } : undefined,
|
|
860
|
+
};
|
|
861
|
+
case "cycle:recent":
|
|
862
|
+
return {
|
|
863
|
+
ok: true,
|
|
864
|
+
command,
|
|
865
|
+
input: rest[0] ? { limit: Number(rest[0]) } : undefined,
|
|
866
|
+
};
|
|
789
867
|
default:
|
|
790
868
|
return {
|
|
791
869
|
ok: true,
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "second-nature",
|
|
3
3
|
"name": "Second Nature",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.26",
|
|
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 (see README / T1.1.4 ops norm).",
|
|
6
6
|
"activation": {
|
|
7
7
|
"onStartup": true,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@haaaiawd/second-nature",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.26",
|
|
4
4
|
"description": "OpenClaw native plugin with synchronous registration, a packaged runtime artifact, and operator-facing status/explain flows.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"openclaw",
|
|
@@ -33,11 +33,11 @@
|
|
|
33
33
|
"./index.js"
|
|
34
34
|
],
|
|
35
35
|
"compat": {
|
|
36
|
-
"pluginApi": ">=2026.5.
|
|
36
|
+
"pluginApi": ">=2026.5.12"
|
|
37
37
|
}
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
40
|
-
"openclaw": ">=2026.5.
|
|
40
|
+
"openclaw": ">=2026.5.12"
|
|
41
41
|
},
|
|
42
42
|
"peerDependenciesMeta": {
|
|
43
43
|
"openclaw": {
|
|
@@ -45,8 +45,8 @@
|
|
|
45
45
|
}
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"drizzle-orm": "^0.
|
|
48
|
+
"drizzle-orm": "^0.45.2",
|
|
49
49
|
"sql.js": "^1.14.1",
|
|
50
|
-
"zod": "^4.
|
|
50
|
+
"zod": "^4.4.3"
|
|
51
51
|
}
|
|
52
52
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { StateDatabase } from "../../storage/db/index.js";
|
|
2
|
+
export interface GoalCommandInput {
|
|
3
|
+
action: "set" | "list" | "accept" | "reject";
|
|
4
|
+
goalId?: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
completionCriteria?: string;
|
|
7
|
+
/** T1.4.2 — alias for `completionCriteria`. */
|
|
8
|
+
criteria?: string;
|
|
9
|
+
risk?: "low" | "medium" | "high";
|
|
10
|
+
kind?: "short_term" | "long_term";
|
|
11
|
+
statusFilter?: string;
|
|
12
|
+
originFilter?: string;
|
|
13
|
+
limit?: number;
|
|
14
|
+
}
|
|
15
|
+
export interface GoalCommandResult {
|
|
16
|
+
ok: boolean;
|
|
17
|
+
command: "goal";
|
|
18
|
+
action: string;
|
|
19
|
+
data?: unknown;
|
|
20
|
+
error?: {
|
|
21
|
+
code: string;
|
|
22
|
+
message: string;
|
|
23
|
+
requiredUserInput?: string[];
|
|
24
|
+
nextStep?: string;
|
|
25
|
+
};
|
|
26
|
+
[key: string]: unknown;
|
|
27
|
+
}
|
|
28
|
+
export declare function goalCommand(stateDb: StateDatabase | undefined, input: GoalCommandInput): Promise<GoalCommandResult>;
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { createAgentGoalStore } from "../../storage/goal/agent-goal-store.js";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
function createGoalCommandError(code, message, requiredUserInput, nextStep) {
|
|
4
|
+
return {
|
|
5
|
+
ok: false,
|
|
6
|
+
command: "goal",
|
|
7
|
+
action: "unknown",
|
|
8
|
+
error: {
|
|
9
|
+
code,
|
|
10
|
+
message,
|
|
11
|
+
requiredUserInput,
|
|
12
|
+
nextStep,
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
function serializeGoal(goal) {
|
|
17
|
+
return {
|
|
18
|
+
goalId: goal.goalId,
|
|
19
|
+
kind: goal.kind,
|
|
20
|
+
status: goal.status,
|
|
21
|
+
origin: goal.origin,
|
|
22
|
+
description: goal.description,
|
|
23
|
+
completionCriteria: goal.completionCriteria,
|
|
24
|
+
risk: goal.risk,
|
|
25
|
+
priorityHint: goal.priorityHint,
|
|
26
|
+
sourceRefs: goal.sourceRefs,
|
|
27
|
+
acceptedBy: goal.acceptedBy,
|
|
28
|
+
createdAt: goal.createdAt,
|
|
29
|
+
updatedAt: goal.updatedAt,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export async function goalCommand(stateDb, input) {
|
|
33
|
+
if (!stateDb) {
|
|
34
|
+
return createGoalCommandError("STATE_UNAVAILABLE", "goal command requires StateDatabase to be wired into OpsRouterDeps", [], "wire_state_into_ops_router");
|
|
35
|
+
}
|
|
36
|
+
const store = createAgentGoalStore(stateDb);
|
|
37
|
+
const action = input.action;
|
|
38
|
+
switch (action) {
|
|
39
|
+
case "set": {
|
|
40
|
+
const description = input.description?.trim();
|
|
41
|
+
if (!description || description.length === 0) {
|
|
42
|
+
return createGoalCommandError("MISSING_DESCRIPTION", "goal set requires description", ["description"], "reinvoke_goal_set_with_description");
|
|
43
|
+
}
|
|
44
|
+
const goalId = input.goalId?.trim() || randomUUID();
|
|
45
|
+
const now = new Date().toISOString();
|
|
46
|
+
// T1.4.2: `criteria` is an alias for `completionCriteria`.
|
|
47
|
+
const completionCriteria = input.completionCriteria?.trim() ||
|
|
48
|
+
input.criteria?.trim() ||
|
|
49
|
+
"";
|
|
50
|
+
await store.upsertAgentGoal({
|
|
51
|
+
goalId,
|
|
52
|
+
kind: input.kind ?? "short_term",
|
|
53
|
+
status: "accepted",
|
|
54
|
+
origin: "owner_set",
|
|
55
|
+
description,
|
|
56
|
+
completionCriteria,
|
|
57
|
+
risk: input.risk ?? "low",
|
|
58
|
+
priorityHint: 0,
|
|
59
|
+
sourceRefs: [],
|
|
60
|
+
acceptedBy: "owner",
|
|
61
|
+
createdAt: now,
|
|
62
|
+
updatedAt: now,
|
|
63
|
+
});
|
|
64
|
+
const created = await store.loadAgentGoal(goalId);
|
|
65
|
+
return {
|
|
66
|
+
ok: true,
|
|
67
|
+
command: "goal",
|
|
68
|
+
action: "set",
|
|
69
|
+
data: {
|
|
70
|
+
goal: created ? serializeGoal(created) : null,
|
|
71
|
+
before: null,
|
|
72
|
+
after: { status: "accepted", origin: "owner_set", acceptedBy: "owner" },
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
case "list": {
|
|
77
|
+
const statuses = input.statusFilter
|
|
78
|
+
? input.statusFilter.split(",").map((s) => s.trim())
|
|
79
|
+
: undefined;
|
|
80
|
+
const origins = input.originFilter
|
|
81
|
+
? input.originFilter.split(",").map((s) => s.trim())
|
|
82
|
+
: undefined;
|
|
83
|
+
const goals = await store.listAgentGoals({
|
|
84
|
+
statuses,
|
|
85
|
+
origins,
|
|
86
|
+
limit: input.limit ?? 50,
|
|
87
|
+
});
|
|
88
|
+
return {
|
|
89
|
+
ok: true,
|
|
90
|
+
command: "goal",
|
|
91
|
+
action: "list",
|
|
92
|
+
data: {
|
|
93
|
+
total: goals.length,
|
|
94
|
+
goals: goals.map(serializeGoal),
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
case "accept": {
|
|
99
|
+
const goalId = input.goalId?.trim();
|
|
100
|
+
if (!goalId) {
|
|
101
|
+
return createGoalCommandError("MISSING_GOAL_ID", "goal accept requires goalId", ["goalId"], "reinvoke_goal_accept_with_goalId");
|
|
102
|
+
}
|
|
103
|
+
const before = await store.loadAgentGoal(goalId);
|
|
104
|
+
if (!before) {
|
|
105
|
+
return createGoalCommandError("GOAL_NOT_FOUND", `No goal found for goalId: ${goalId}`, ["goalId"], "verify_goal_id_or_run_goal_list");
|
|
106
|
+
}
|
|
107
|
+
if (before.status !== "proposal") {
|
|
108
|
+
return createGoalCommandError("INVALID_STATUS_TRANSITION", `Cannot accept goal with status '${before.status}'. Only 'proposal' goals can be accepted.`, ["goalId"], "verify_goal_status_or_run_goal_list");
|
|
109
|
+
}
|
|
110
|
+
await store.transitionGoalStatus({
|
|
111
|
+
goalId,
|
|
112
|
+
newStatus: "accepted",
|
|
113
|
+
acceptedBy: "owner",
|
|
114
|
+
updatedAt: new Date().toISOString(),
|
|
115
|
+
});
|
|
116
|
+
const after = await store.loadAgentGoal(goalId);
|
|
117
|
+
return {
|
|
118
|
+
ok: true,
|
|
119
|
+
command: "goal",
|
|
120
|
+
action: "accept",
|
|
121
|
+
data: {
|
|
122
|
+
goalId,
|
|
123
|
+
before: { status: before.status, origin: before.origin },
|
|
124
|
+
after: after
|
|
125
|
+
? { status: after.status, origin: after.origin, acceptedBy: after.acceptedBy }
|
|
126
|
+
: null,
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
case "reject": {
|
|
131
|
+
const goalId = input.goalId?.trim();
|
|
132
|
+
if (!goalId) {
|
|
133
|
+
return createGoalCommandError("MISSING_GOAL_ID", "goal reject requires goalId", ["goalId"], "reinvoke_goal_reject_with_goalId");
|
|
134
|
+
}
|
|
135
|
+
const before = await store.loadAgentGoal(goalId);
|
|
136
|
+
if (!before) {
|
|
137
|
+
return createGoalCommandError("GOAL_NOT_FOUND", `No goal found for goalId: ${goalId}`, ["goalId"], "verify_goal_id_or_run_goal_list");
|
|
138
|
+
}
|
|
139
|
+
if (before.status !== "proposal") {
|
|
140
|
+
return createGoalCommandError("INVALID_STATUS_TRANSITION", `Cannot reject goal with status '${before.status}'. Only 'proposal' goals can be rejected.`, ["goalId"], "verify_goal_status_or_run_goal_list");
|
|
141
|
+
}
|
|
142
|
+
await store.transitionGoalStatus({
|
|
143
|
+
goalId,
|
|
144
|
+
newStatus: "rejected",
|
|
145
|
+
updatedAt: new Date().toISOString(),
|
|
146
|
+
});
|
|
147
|
+
const after = await store.loadAgentGoal(goalId);
|
|
148
|
+
return {
|
|
149
|
+
ok: true,
|
|
150
|
+
command: "goal",
|
|
151
|
+
action: "reject",
|
|
152
|
+
data: {
|
|
153
|
+
goalId,
|
|
154
|
+
before: { status: before.status, origin: before.origin },
|
|
155
|
+
after: after ? { status: after.status, origin: after.origin } : null,
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
default: {
|
|
160
|
+
return createGoalCommandError("UNKNOWN_GOAL_ACTION", `Unknown goal action: ${action}. Supported: set, list, accept, reject.`, ["action"], "reinvoke_with_supported_action");
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
@@ -26,10 +26,10 @@ export function createCliCommands(deps) {
|
|
|
26
26
|
return [
|
|
27
27
|
{
|
|
28
28
|
name: "status",
|
|
29
|
-
description: "Show aggregated Second Nature status",
|
|
29
|
+
description: "T1.2.6 — Show v6 aggregated Second Nature status (narrative + dream + cycles + runtime)",
|
|
30
30
|
execute: async (input) => {
|
|
31
31
|
const scope = typeof input?.scope === "string" ? input.scope : undefined;
|
|
32
|
-
const data = await readModels.
|
|
32
|
+
const data = await readModels.loadV6Status(scope);
|
|
33
33
|
return { ok: true, data };
|
|
34
34
|
},
|
|
35
35
|
},
|
|
@@ -136,7 +136,7 @@ export function createCliCommands(deps) {
|
|
|
136
136
|
return explainSubjectError("EXPLAIN_SUBJECT_REQUIRES_ID", "subject must include identifier");
|
|
137
137
|
}
|
|
138
138
|
if (code === "explain_subject_unsupported") {
|
|
139
|
-
return explainSubjectError("EXPLAIN_SUBJECT_UNSUPPORTED", "supported subjects include decision:, platform:, outreach:, soul:, fallback:, delivery:, probe:, report:, source:");
|
|
139
|
+
return explainSubjectError("EXPLAIN_SUBJECT_UNSUPPORTED", "supported subjects include decision:, platform:, outreach:, soul:, fallback:, delivery:, probe:, report:, source:, relationship:");
|
|
140
140
|
}
|
|
141
141
|
return explainSubjectError("EXPLAIN_SUBJECT_INVALID", "invalid explain subject");
|
|
142
142
|
}
|
|
@@ -260,5 +260,40 @@ export function createCliCommands(deps) {
|
|
|
260
260
|
return surface;
|
|
261
261
|
},
|
|
262
262
|
},
|
|
263
|
+
{
|
|
264
|
+
name: "goal",
|
|
265
|
+
description: "T1.2.4 — owner-governed goal operations: set, list, accept, reject",
|
|
266
|
+
execute: async (input) => {
|
|
267
|
+
const surface = await Promise.resolve(opsRouter.dispatch("goal", input));
|
|
268
|
+
return surface;
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
name: "narrative",
|
|
273
|
+
description: "T1.2.1 — show current NarrativeState: focus, progress, next intent, source refs, grounding status",
|
|
274
|
+
execute: async (input) => {
|
|
275
|
+
const narrativeId = typeof input?.narrativeId === "string" ? input.narrativeId : undefined;
|
|
276
|
+
const data = await readModels.loadNarrative(narrativeId);
|
|
277
|
+
return { ok: true, data };
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
name: "dream:recent",
|
|
282
|
+
description: "T1.2.2 — show recent Dream run results, candidate/accepted status, fallback/partial summary",
|
|
283
|
+
execute: async (input) => {
|
|
284
|
+
const limit = typeof input?.limit === "number" ? input.limit : 5;
|
|
285
|
+
const data = await readModels.loadDreamRecent(limit);
|
|
286
|
+
return { ok: true, data };
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
name: "cycle:recent",
|
|
291
|
+
description: "T1.2.5 — aggregate recent heartbeat, narrative, Dream, delivery, connector cycle summary",
|
|
292
|
+
execute: async (input) => {
|
|
293
|
+
const limit = typeof input?.limit === "number" ? input.limit : 5;
|
|
294
|
+
const data = await readModels.loadCycleRecent(limit);
|
|
295
|
+
return { ok: true, data };
|
|
296
|
+
},
|
|
297
|
+
},
|
|
263
298
|
];
|
|
264
299
|
}
|
|
@@ -37,5 +37,8 @@ export function resolveExplainSubject(raw) {
|
|
|
37
37
|
if (prefix === "source" || prefix === "source_ref") {
|
|
38
38
|
return { kind: "source_ref", id };
|
|
39
39
|
}
|
|
40
|
+
if (prefix === "relationship") {
|
|
41
|
+
return { kind: "relationship", id };
|
|
42
|
+
}
|
|
40
43
|
throw new Error("explain_subject_unsupported");
|
|
41
44
|
}
|
|
@@ -38,6 +38,6 @@ export interface OpsRouterDeps {
|
|
|
38
38
|
}
|
|
39
39
|
export interface OpsRouter {
|
|
40
40
|
heartbeatCheck(input: HeartbeatCheckInput): Promise<HeartbeatSurfaceResult>;
|
|
41
|
-
dispatch(command: string, input?: Record<string, unknown>):
|
|
41
|
+
dispatch(command: string, input?: Record<string, unknown>): Promise<HeartbeatSurfaceResult | Record<string, unknown>>;
|
|
42
42
|
}
|
|
43
43
|
export declare function createOpsRouter(deps: OpsRouterDeps): OpsRouter;
|
|
@@ -8,6 +8,7 @@ import { recordHostCapability } from "../host-capability/record-host-capability.
|
|
|
8
8
|
import { runNearRealConnectorSmoke } from "../../connectors/near-real/near-real-connector-smoke.js";
|
|
9
9
|
import { connectorInit } from "../commands/connector-init.js";
|
|
10
10
|
import { connectorStatus, connectorTest } from "../commands/connector-status.js";
|
|
11
|
+
import { goalCommand } from "../commands/goal.js";
|
|
11
12
|
function coerceProbeOnlyFlag(input) {
|
|
12
13
|
const v = input?.probeOnly;
|
|
13
14
|
return v === true || v === "true" || v === 1 || v === "1";
|
|
@@ -45,7 +46,7 @@ export function createOpsRouter(deps) {
|
|
|
45
46
|
workspaceRoot: input.workspaceRoot ?? deps.workspaceRoot,
|
|
46
47
|
connectorExecutor: input.connectorExecutor ?? deps.connectorExecutor,
|
|
47
48
|
}),
|
|
48
|
-
dispatch(command, input) {
|
|
49
|
+
async dispatch(command, input) {
|
|
49
50
|
if (command === "heartbeat_check") {
|
|
50
51
|
const runtimeAvailable = typeof input?.runtimeAvailable === "boolean"
|
|
51
52
|
? input.runtimeAvailable
|
|
@@ -213,6 +214,67 @@ export function createOpsRouter(deps) {
|
|
|
213
214
|
dryRun: input?.dryRun === false ? false : true, // default dry-run
|
|
214
215
|
});
|
|
215
216
|
}
|
|
217
|
+
if (command === "goal") {
|
|
218
|
+
const rawAction = typeof input?.action === "string" ? input.action : "list";
|
|
219
|
+
const action = ["set", "list", "accept", "reject"].includes(rawAction)
|
|
220
|
+
? rawAction
|
|
221
|
+
: "list";
|
|
222
|
+
const sanitizeText = (v, maxLen = 1000) => {
|
|
223
|
+
if (typeof v !== "string")
|
|
224
|
+
return undefined;
|
|
225
|
+
const trimmed = v.trim();
|
|
226
|
+
if (trimmed.length === 0)
|
|
227
|
+
return undefined;
|
|
228
|
+
return trimmed.slice(0, maxLen);
|
|
229
|
+
};
|
|
230
|
+
return goalCommand(deps.state, {
|
|
231
|
+
action,
|
|
232
|
+
goalId: typeof input?.goalId === "string" ? input.goalId.trim().slice(0, 128) : undefined,
|
|
233
|
+
description: sanitizeText(input?.description),
|
|
234
|
+
completionCriteria: sanitizeText(input?.completionCriteria),
|
|
235
|
+
// T1.4.2: criteria alias for completionCriteria
|
|
236
|
+
criteria: sanitizeText(input?.criteria),
|
|
237
|
+
risk: typeof input?.risk === "string"
|
|
238
|
+
? input.risk
|
|
239
|
+
: undefined,
|
|
240
|
+
kind: typeof input?.kind === "string"
|
|
241
|
+
? input.kind
|
|
242
|
+
: undefined,
|
|
243
|
+
statusFilter: typeof input?.statusFilter === "string" ? input.statusFilter : undefined,
|
|
244
|
+
originFilter: typeof input?.originFilter === "string" ? input.originFilter : undefined,
|
|
245
|
+
limit: typeof input?.limit === "number" ? input.limit : undefined,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
if (command === "dream:recent") {
|
|
249
|
+
if (!deps.readModels) {
|
|
250
|
+
return {
|
|
251
|
+
ok: false,
|
|
252
|
+
error: {
|
|
253
|
+
code: "READ_MODELS_UNAVAILABLE",
|
|
254
|
+
message: "dream:recent requires workspace read models",
|
|
255
|
+
nextStep: "wire_read_models_into_ops_router",
|
|
256
|
+
},
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
const limit = typeof input?.limit === "number" ? input.limit : 5;
|
|
260
|
+
const data = await deps.readModels.loadDreamRecent(limit);
|
|
261
|
+
return { ok: true, data };
|
|
262
|
+
}
|
|
263
|
+
if (command === "cycle:recent") {
|
|
264
|
+
if (!deps.readModels) {
|
|
265
|
+
return {
|
|
266
|
+
ok: false,
|
|
267
|
+
error: {
|
|
268
|
+
code: "READ_MODELS_UNAVAILABLE",
|
|
269
|
+
message: "cycle:recent requires workspace read models",
|
|
270
|
+
nextStep: "wire_read_models_into_ops_router",
|
|
271
|
+
},
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
const limit = typeof input?.limit === "number" ? input.limit : 5;
|
|
275
|
+
const data = await deps.readModels.loadCycleRecent(limit);
|
|
276
|
+
return { ok: true, data };
|
|
277
|
+
}
|
|
216
278
|
return {
|
|
217
279
|
ok: false,
|
|
218
280
|
error: {
|
|
@@ -18,6 +18,7 @@ import type { CliReadModels } from "../read-models/index.js";
|
|
|
18
18
|
import type { RuntimeDecisionRecorder } from "../../observability/services/runtime-decision-recorder.js";
|
|
19
19
|
import type { StateDatabase } from "../../storage/db/index.js";
|
|
20
20
|
import type { ConnectorExecutor } from "../../core/second-nature/orchestrator/effect-dispatcher.js";
|
|
21
|
+
import type { CapabilityContractRegistry } from "../../connectors/base/manifest.js";
|
|
21
22
|
export interface WorkspaceHeartbeatRunnerOptions {
|
|
22
23
|
/** When supplied, the runner persists the cycle so `loadStatus` can read it (T1.2.3). */
|
|
23
24
|
runtimeRecorder?: RuntimeDecisionRecorder;
|
|
@@ -38,6 +39,11 @@ export interface WorkspaceHeartbeatRunnerOptions {
|
|
|
38
39
|
* connector-system instead of returning connector_dispatch_unwired.
|
|
39
40
|
*/
|
|
40
41
|
connectorExecutor?: ConnectorExecutor;
|
|
42
|
+
/**
|
|
43
|
+
* T2.4.1: when present, planner resolves platform-specific intents from accepted goals
|
|
44
|
+
* and connector evidence.
|
|
45
|
+
*/
|
|
46
|
+
connectorRegistry?: CapabilityContractRegistry;
|
|
41
47
|
}
|
|
42
48
|
export declare function loadSnapshotInputsForWorkspaceHeartbeat(readModels: CliReadModels, options?: {
|
|
43
49
|
state?: StateDatabase;
|
|
@@ -2,6 +2,7 @@ import { runHeartbeatCycle } from "../../core/second-nature/heartbeat/run-heartb
|
|
|
2
2
|
import { loadLifeEvidenceSnapshot } from "../../storage/snapshots/life-evidence-snapshot.js";
|
|
3
3
|
import { createAgentGoalStore } from "../../storage/goal/agent-goal-store.js";
|
|
4
4
|
import { createNarrativeStateStore } from "../../storage/narrative/narrative-state-store.js";
|
|
5
|
+
import { createRelationshipMemoryStore } from "../../storage/relationship/relationship-memory-store.js";
|
|
5
6
|
export async function loadSnapshotInputsForWorkspaceHeartbeat(readModels, options = {}) {
|
|
6
7
|
const status = await readModels.loadStatus();
|
|
7
8
|
const mode = status.rhythm.mode === "unknown" ? "active" : status.rhythm.mode;
|
|
@@ -30,6 +31,8 @@ export async function loadSnapshotInputsForWorkspaceHeartbeat(readModels, option
|
|
|
30
31
|
platformEventCount = snapshot.platformEvents.length;
|
|
31
32
|
workEventCount = snapshot.workEvents.length;
|
|
32
33
|
if (snapshot.empty) {
|
|
34
|
+
// L-01: Currently snapshot only exposes `empty` boolean.
|
|
35
|
+
// Future: if snapshot adds `emptyReason` (e.g. "redacted_only"), map it here.
|
|
33
36
|
lifeEvidenceEmptyReason = "no_sources";
|
|
34
37
|
}
|
|
35
38
|
}
|
|
@@ -46,7 +49,9 @@ export async function loadSnapshotInputsForWorkspaceHeartbeat(readModels, option
|
|
|
46
49
|
lifeEvidenceEmptyReason = "state_unavailable";
|
|
47
50
|
}
|
|
48
51
|
// T2.1.4: Load accepted goals from state DB when available.
|
|
52
|
+
// M-03: typed as GoalContext to avoid coupling to the full AgentGoal schema.
|
|
49
53
|
let acceptedGoals;
|
|
54
|
+
let acceptedGoalsLoadError;
|
|
50
55
|
if (options.state) {
|
|
51
56
|
try {
|
|
52
57
|
const goalStore = createAgentGoalStore(options.state);
|
|
@@ -55,8 +60,29 @@ export async function loadSnapshotInputsForWorkspaceHeartbeat(readModels, option
|
|
|
55
60
|
limit: 20,
|
|
56
61
|
});
|
|
57
62
|
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
acceptedGoals = [];
|
|
65
|
+
acceptedGoalsLoadError = err instanceof Error ? err.message : String(err);
|
|
66
|
+
// H-05: Distinguish "load failed" from "no goals" for observability.
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// CR-02: Load narrative state and relationship memory when state is available.
|
|
70
|
+
let narrativeState;
|
|
71
|
+
let relationshipMemory;
|
|
72
|
+
if (options.state) {
|
|
73
|
+
try {
|
|
74
|
+
const narrativeStore = createNarrativeStateStore(options.state);
|
|
75
|
+
narrativeState = (await narrativeStore.loadNarrativeState()) ?? undefined;
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// Narrative state is optional; failure should not block the cycle.
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
const relationshipStore = createRelationshipMemoryStore(options.state);
|
|
82
|
+
relationshipMemory = (await relationshipStore.loadRelationshipMemory()) ?? undefined;
|
|
83
|
+
}
|
|
58
84
|
catch {
|
|
59
|
-
|
|
85
|
+
// Relationship memory is optional; failure should not block the cycle.
|
|
60
86
|
}
|
|
61
87
|
}
|
|
62
88
|
return {
|
|
@@ -74,6 +100,9 @@ export async function loadSnapshotInputsForWorkspaceHeartbeat(readModels, option
|
|
|
74
100
|
workEventCount,
|
|
75
101
|
lifeEvidenceEmptyReason,
|
|
76
102
|
acceptedGoals,
|
|
103
|
+
acceptedGoalsLoadError,
|
|
104
|
+
narrativeState,
|
|
105
|
+
relationshipMemory,
|
|
77
106
|
};
|
|
78
107
|
}
|
|
79
108
|
export function createWorkspaceHeartbeatRunner(readModels, options = {}) {
|
|
@@ -99,6 +128,11 @@ export function createWorkspaceHeartbeatRunner(readModels, options = {}) {
|
|
|
99
128
|
: undefined,
|
|
100
129
|
connectorExecutor: options.connectorExecutor,
|
|
101
130
|
narrativeStateStore,
|
|
131
|
+
// T3.3.1: pass state + workspaceRoot so connector effects can write life evidence.
|
|
132
|
+
state: options.state,
|
|
133
|
+
workspaceRoot: options.workspaceRoot,
|
|
134
|
+
// T2.4.1: pass registry so planner resolves platform-specific intents.
|
|
135
|
+
connectorRegistry: options.connectorRegistry,
|
|
102
136
|
},
|
|
103
137
|
});
|
|
104
138
|
if (options.runtimeRecorder) {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { StateDatabase } from "../../storage/db/index.js";
|
|
2
2
|
import type { ObservabilityDatabase } from "../../observability/db/index.js";
|
|
3
3
|
import { AppendOnlyAuditStore } from "../../observability/audit/append-only-audit-store.js";
|
|
4
|
-
import type { StatusReadModel, DailyReportReadModel, QuietReadModel, SessionDetailReadModel, CredentialReadModel, ExplainReadModel, ExplainSubjectKind, AuditSummaryReadModel } from "./types.js";
|
|
5
|
-
export type { AuditSummaryReadModel } from "./types.js";
|
|
4
|
+
import type { StatusReadModel, DailyReportReadModel, QuietReadModel, SessionDetailReadModel, CredentialReadModel, ExplainReadModel, ExplainSubjectKind, AuditSummaryReadModel, DreamRecentReadModel, CycleRecentReadModel, NarrativeReadModel, StatusV6ReadModel } from "./types.js";
|
|
5
|
+
export type { AuditSummaryReadModel, StatusV6ReadModel } from "./types.js";
|
|
6
6
|
export type { ExplainSubjectKind } from "./types.js";
|
|
7
7
|
import type { OperatorFallbackView } from "../../storage/fallback/operator-fallback-view.js";
|
|
8
8
|
import { type RhythmPolicySnapshot } from "../../storage/rhythm/rhythm-policy-snapshot.js";
|
|
@@ -24,6 +24,18 @@ export interface CliReadModels {
|
|
|
24
24
|
* Empty store returns `{ totalEvents: 0, events: [] }` (honest empty, not an error).
|
|
25
25
|
*/
|
|
26
26
|
loadAuditSummary(): Promise<AuditSummaryReadModel>;
|
|
27
|
+
/** T1.2.2 — recent Dream runs from audit store. */
|
|
28
|
+
loadDreamRecent(limit?: number): Promise<DreamRecentReadModel>;
|
|
29
|
+
/** T1.2.5 — recent cycle summary from audit store. */
|
|
30
|
+
loadCycleRecent(limit?: number): Promise<CycleRecentReadModel>;
|
|
31
|
+
/** T1.2.1 — current NarrativeState; returns nothing_yet when no data exists. */
|
|
32
|
+
loadNarrative(narrativeId?: string): Promise<NarrativeReadModel>;
|
|
33
|
+
/**
|
|
34
|
+
* T1.2.6 — v6 status aggregate: StatusReadModel extended with narrative, dream recent,
|
|
35
|
+
* and cycle recent sections. Each section has a sentinel status (nothing_yet / has_runs /
|
|
36
|
+
* has_cycles) so operators always get a meaningful, non-empty response.
|
|
37
|
+
*/
|
|
38
|
+
loadV6Status(scope?: string): Promise<StatusV6ReadModel>;
|
|
27
39
|
}
|
|
28
40
|
/** T1.2.1 / T1.2.2 — operator-facing read surface (subset of full CLI read models). */
|
|
29
41
|
export type OpsReadModelPort = Pick<CliReadModels, "loadStatus" | "loadDailyReport" | "loadQuiet" | "loadSession" | "loadCredential" | "explain" | "loadFallbackView">;
|