@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.
Files changed (47) hide show
  1. package/index.js +78 -0
  2. package/openclaw.plugin.json +1 -1
  3. package/package.json +5 -5
  4. package/runtime/cli/commands/goal.d.ts +28 -0
  5. package/runtime/cli/commands/goal.js +163 -0
  6. package/runtime/cli/commands/index.js +38 -3
  7. package/runtime/cli/explain/resolve-subject.js +3 -0
  8. package/runtime/cli/ops/ops-router.d.ts +1 -1
  9. package/runtime/cli/ops/ops-router.js +63 -1
  10. package/runtime/cli/ops/workspace-heartbeat-runner.d.ts +6 -0
  11. package/runtime/cli/ops/workspace-heartbeat-runner.js +35 -1
  12. package/runtime/cli/read-models/index.d.ts +14 -2
  13. package/runtime/cli/read-models/index.js +403 -101
  14. package/runtime/cli/read-models/types.d.ts +90 -3
  15. package/runtime/core/second-nature/feedback/owner-reply-feedback.d.ts +46 -0
  16. package/runtime/core/second-nature/feedback/owner-reply-feedback.js +159 -0
  17. package/runtime/core/second-nature/heartbeat/heartbeat-loop.d.ts +11 -1
  18. package/runtime/core/second-nature/heartbeat/heartbeat-loop.js +78 -10
  19. package/runtime/core/second-nature/heartbeat/runtime-snapshot.d.ts +2 -0
  20. package/runtime/core/second-nature/heartbeat/runtime-snapshot.js +1 -1
  21. package/runtime/core/second-nature/heartbeat/snapshot-builder.d.ts +16 -2
  22. package/runtime/core/second-nature/index.d.ts +1 -0
  23. package/runtime/core/second-nature/index.js +1 -0
  24. package/runtime/core/second-nature/orchestrator/goal-priority.d.ts +16 -3
  25. package/runtime/core/second-nature/orchestrator/goal-priority.js +10 -9
  26. package/runtime/core/second-nature/orchestrator/intent-planner.d.ts +29 -1
  27. package/runtime/core/second-nature/orchestrator/intent-planner.js +154 -79
  28. package/runtime/core/second-nature/orchestrator/narrative-update.js +23 -9
  29. package/runtime/core/second-nature/orchestrator/platform-capability-router.d.ts +34 -0
  30. package/runtime/core/second-nature/orchestrator/platform-capability-router.js +115 -0
  31. package/runtime/core/second-nature/outreach/build-outreach-draft-request.d.ts +3 -1
  32. package/runtime/core/second-nature/outreach/build-outreach-draft-request.js +39 -1
  33. package/runtime/core/second-nature/outreach/dispatch-user-outreach.js +21 -2
  34. package/runtime/guidance/draft-outreach-message.js +14 -1
  35. package/runtime/guidance/outreach-draft-schema.d.ts +104 -0
  36. package/runtime/guidance/outreach-draft-schema.js +14 -0
  37. package/runtime/observability/audit/audit-envelope.d.ts +1 -1
  38. package/runtime/observability/query/explain-query.d.ts +3 -0
  39. package/runtime/observability/query/explain-query.js +9 -0
  40. package/runtime/observability/services/lived-experience-audit.d.ts +22 -0
  41. package/runtime/observability/services/lived-experience-audit.js +30 -0
  42. package/runtime/shared/types/credential.d.ts +1 -1
  43. package/runtime/storage/chronicle/session-chronicle-store.d.ts +1 -1
  44. package/runtime/storage/db/schema/narrative-state.d.ts +1 -1
  45. package/runtime/storage/db/schema/narrative-state.js +2 -2
  46. package/runtime/storage/services/credential-vault.d.ts +18 -0
  47. 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,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "second-nature",
3
3
  "name": "Second Nature",
4
- "version": "0.1.23",
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.24",
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.4"
36
+ "pluginApi": ">=2026.5.12"
37
37
  }
38
38
  },
39
39
  "peerDependencies": {
40
- "openclaw": ">=2026.5.4"
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.44.4",
48
+ "drizzle-orm": "^0.45.2",
49
49
  "sql.js": "^1.14.1",
50
- "zod": "^4.1.5"
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.loadStatus(scope);
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>): HeartbeatSurfaceResult | Record<string, unknown> | Promise<HeartbeatSurfaceResult> | Promise<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
- acceptedGoals = undefined;
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">;