@haaaiawd/second-nature 0.1.3 → 0.1.5

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 (31) hide show
  1. package/index.js +151 -0
  2. package/openclaw.plugin.json +2 -2
  3. package/package.json +4 -4
  4. package/runtime/connectors/social-community/moltbook/api-client.d.ts +27 -0
  5. package/runtime/connectors/social-community/moltbook/api-client.js +71 -0
  6. package/runtime/connectors/social-community/moltbook/index.d.ts +1 -0
  7. package/runtime/connectors/social-community/moltbook/index.js +1 -0
  8. package/runtime/core/second-nature/guidance/user-reply-continuity.d.ts +50 -0
  9. package/runtime/core/second-nature/guidance/user-reply-continuity.js +80 -0
  10. package/runtime/core/second-nature/heartbeat/heartbeat-executor.d.ts +97 -0
  11. package/runtime/core/second-nature/heartbeat/heartbeat-executor.js +112 -0
  12. package/runtime/core/second-nature/heartbeat/heartbeat-loop.d.ts +42 -0
  13. package/runtime/core/second-nature/heartbeat/heartbeat-loop.js +73 -0
  14. package/runtime/core/second-nature/heartbeat/index.d.ts +5 -0
  15. package/runtime/core/second-nature/heartbeat/index.js +4 -0
  16. package/runtime/core/second-nature/heartbeat/scope-router.d.ts +28 -0
  17. package/runtime/core/second-nature/heartbeat/scope-router.js +46 -0
  18. package/runtime/core/second-nature/heartbeat/signal.d.ts +35 -0
  19. package/runtime/core/second-nature/heartbeat/signal.js +8 -0
  20. package/runtime/core/second-nature/heartbeat/snapshot-builder.d.ts +33 -0
  21. package/runtime/core/second-nature/heartbeat/snapshot-builder.js +18 -0
  22. package/runtime/core/second-nature/runtime/service-entry.js +1 -1
  23. package/runtime/guidance/guidance-assembler.js +1 -1
  24. package/runtime/guidance/persona-selection.js +5 -0
  25. package/runtime/guidance/template-registry.d.ts +1 -1
  26. package/runtime/guidance/types.d.ts +1 -1
  27. package/runtime/observability/index.d.ts +1 -1
  28. package/runtime/observability/services/decision-ledger.d.ts +13 -0
  29. package/runtime/observability/services/decision-ledger.js +42 -0
  30. package/index.ts +0 -200
  31. package/runtime/setup/HOST_SETUP.md +0 -112
@@ -0,0 +1,73 @@
1
+ import { buildContinuitySnapshot } from "./snapshot-builder.js";
2
+ import { planIntent } from "../orchestrator/intent-planner.js";
3
+ import { evaluateGuards } from "../orchestrator/guard-layer.js";
4
+ /**
5
+ * Ingest a heartbeat rhythm signal and drive one full decision round.
6
+ *
7
+ * Decision flow:
8
+ * 1. Build continuity snapshot from state-system inputs
9
+ * 2. Plan candidate intents from snapshot
10
+ * 3. Evaluate guards for each candidate in priority order
11
+ * 4. Return one of:
12
+ * - intent_selected: a candidate passed all guards
13
+ * - denied: candidates existed but all were rejected by guards
14
+ * - heartbeat_ok: no candidates or no action warranted (conservative default)
15
+ *
16
+ * Per ADR-005: heartbeat is the free-rhythm main entry; this loop
17
+ * implements the default conservative path where HEARTBEAT_OK is
18
+ * the first-class result when no action is warranted.
19
+ */
20
+ export async function ingestRhythmSignal(signal, deps) {
21
+ // Step 1: Build continuity snapshot
22
+ const inputs = await deps.loadSnapshotInputs();
23
+ const snapshot = buildContinuitySnapshot(inputs);
24
+ // Step 2: Plan candidate intents
25
+ const candidates = planIntent(snapshot);
26
+ // Step 3: Evaluate guards for each candidate (priority order)
27
+ let hasCandidates = false;
28
+ let anyAllow = false;
29
+ const denyReasons = [];
30
+ for (const intent of candidates) {
31
+ hasCandidates = true;
32
+ const evaluation = evaluateGuards(intent, snapshot);
33
+ if (evaluation.verdict === "allow") {
34
+ anyAllow = true;
35
+ return {
36
+ scope: "rhythm",
37
+ status: "intent_selected",
38
+ selectedIntentId: intent.id,
39
+ reasons: evaluation.reasons,
40
+ };
41
+ }
42
+ denyReasons.push(`${intent.id}:${evaluation.verdict}(${evaluation.reasons.join(",")})`);
43
+ }
44
+ // Step 4: No viable intent path
45
+ if (!hasCandidates) {
46
+ // No candidates at all → heartbeat_ok (nothing to do)
47
+ return {
48
+ scope: "rhythm",
49
+ status: "heartbeat_ok",
50
+ reasons: ["no_candidates"],
51
+ };
52
+ }
53
+ if (!anyAllow && denyReasons.length > 0) {
54
+ // Candidates existed but all denied/deferred/escalated → denied
55
+ return {
56
+ scope: "rhythm",
57
+ status: "denied",
58
+ reasons: denyReasons,
59
+ };
60
+ }
61
+ // Fallback: conservative heartbeat_ok
62
+ return {
63
+ scope: "rhythm",
64
+ status: "heartbeat_ok",
65
+ reasons: ["no_allow_verdict"],
66
+ };
67
+ }
68
+ /**
69
+ * Build a snapshot directly from inputs (for testing or when state-system is unavailable).
70
+ */
71
+ export function buildSnapshotFromInputs(inputs) {
72
+ return buildContinuitySnapshot(inputs);
73
+ }
@@ -0,0 +1,5 @@
1
+ export { type RuntimeScope, type RuntimeTrigger, type HeartbeatCycleStatus, type HeartbeatSignal, type ScopedRuntimeInput, type HeartbeatCycleResult, type ScopeRouteResult, } from "./signal.js";
2
+ export { buildContinuitySnapshot, type SnapshotInputs, } from "./snapshot-builder.js";
3
+ export { ingestRhythmSignal, type HeartbeatDeps, buildSnapshotFromInputs, } from "./heartbeat-loop.js";
4
+ export { routeScopedInput, type ScopeRouterDeps, } from "./scope-router.js";
5
+ export { requestGuidanceForIntent, dispatchAllowedEffect, executeHeartbeatCycle, type GuidanceBridgeDeps, type EffectDispatchDeps, type HeartbeatExecutorDeps, type GuidanceBridgeResult, type HeartbeatExecutionResult, } from "./heartbeat-executor.js";
@@ -0,0 +1,4 @@
1
+ export { buildContinuitySnapshot, } from "./snapshot-builder.js";
2
+ export { ingestRhythmSignal, buildSnapshotFromInputs, } from "./heartbeat-loop.js";
3
+ export { routeScopedInput, } from "./scope-router.js";
4
+ export { requestGuidanceForIntent, dispatchAllowedEffect, executeHeartbeatCycle, } from "./heartbeat-executor.js";
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Scope Signal Router
3
+ *
4
+ * Routes incoming signals to the correct runtime scope based on
5
+ * bridge protocol, entry type, or explicit signal metadata.
6
+ *
7
+ * Per ADR-005: runtime scope classification must NOT assume host natural
8
+ * classification. It depends on explicit signal metadata.
9
+ *
10
+ * Three scopes:
11
+ * - rhythm: heartbeat bridge signals, enters free-rhythm decision chain
12
+ * - user_task: user explicit tasks, bypasses rhythm gate entirely
13
+ * - user_reply: direct user replies, only gets very light continuity
14
+ */
15
+ import type { ScopedRuntimeInput, ScopeRouteResult } from "./signal.js";
16
+ export interface ScopeRouterDeps {
17
+ /** Optional: additional context for scope resolution */
18
+ getContext: () => Record<string, unknown>;
19
+ }
20
+ /**
21
+ * Route a scoped input to the correct runtime chain.
22
+ *
23
+ * Classification priority:
24
+ * 1. Explicit scopeHint from the signal
25
+ * 2. Trigger type mapping
26
+ * 3. Default fallback to rhythm for unknown triggers
27
+ */
28
+ export declare function routeScopedInput(input: ScopedRuntimeInput, _deps?: ScopeRouterDeps): ScopeRouteResult;
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Route a scoped input to the correct runtime chain.
3
+ *
4
+ * Classification priority:
5
+ * 1. Explicit scopeHint from the signal
6
+ * 2. Trigger type mapping
7
+ * 3. Default fallback to rhythm for unknown triggers
8
+ */
9
+ export function routeScopedInput(input, _deps) {
10
+ // Priority 1: Use explicit scopeHint if provided
11
+ if (input.scopeHint) {
12
+ return {
13
+ scope: input.scopeHint,
14
+ trigger: input.trigger,
15
+ handled: true,
16
+ };
17
+ }
18
+ // Priority 2: Map trigger type to scope
19
+ const scope = triggerToScope(input.trigger);
20
+ return {
21
+ scope,
22
+ trigger: input.trigger,
23
+ handled: true,
24
+ };
25
+ }
26
+ /**
27
+ * Map a trigger type to its default runtime scope.
28
+ *
29
+ * This is the fallback when no explicit scopeHint is provided.
30
+ */
31
+ function triggerToScope(trigger) {
32
+ switch (trigger) {
33
+ case "heartbeat_bridge":
34
+ return "rhythm";
35
+ case "user_task":
36
+ return "user_task";
37
+ case "user_reply":
38
+ return "user_reply";
39
+ case "interrupt":
40
+ return "rhythm";
41
+ case "resume":
42
+ return "rhythm";
43
+ default:
44
+ return "rhythm";
45
+ }
46
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Heartbeat Signal Contract
3
+ *
4
+ * Defines the signal types that enter the control-plane from various sources.
5
+ * Per ADR-005: runtime scope classification depends on bridge protocol,
6
+ * entry type, or explicit signal metadata, NOT on host natural classification.
7
+ */
8
+ export type RuntimeScope = "rhythm" | "user_task" | "user_reply";
9
+ export type RuntimeTrigger = "heartbeat_bridge" | "user_task" | "user_reply" | "interrupt" | "resume";
10
+ export type HeartbeatCycleStatus = "heartbeat_ok" | "intent_selected" | "deferred" | "denied";
11
+ export interface HeartbeatSignal {
12
+ trigger: RuntimeTrigger;
13
+ scopeHint?: RuntimeScope;
14
+ payload: {
15
+ timestamp: string;
16
+ sessionContext?: string;
17
+ heartbeatChecklist?: string;
18
+ };
19
+ }
20
+ export interface ScopedRuntimeInput {
21
+ trigger: RuntimeTrigger;
22
+ scopeHint?: RuntimeScope;
23
+ payload: Record<string, unknown>;
24
+ }
25
+ export interface HeartbeatCycleResult {
26
+ scope: "rhythm";
27
+ status: HeartbeatCycleStatus;
28
+ selectedIntentId?: string;
29
+ reasons: string[];
30
+ }
31
+ export interface ScopeRouteResult {
32
+ scope: RuntimeScope;
33
+ trigger: RuntimeTrigger;
34
+ handled: boolean;
35
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Heartbeat Signal Contract
3
+ *
4
+ * Defines the signal types that enter the control-plane from various sources.
5
+ * Per ADR-005: runtime scope classification depends on bridge protocol,
6
+ * entry type, or explicit signal metadata, NOT on host natural classification.
7
+ */
8
+ export {};
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Snapshot Builder
3
+ *
4
+ * Builds a ContinuitySnapshot from state-system, workspace, and runtime context.
5
+ * This is the input preparation step for each heartbeat round.
6
+ *
7
+ * Per design doc §4.2: SnapshotBuilder prepares inputs for the Rhythm Engine.
8
+ */
9
+ import type { ContinuitySnapshot, TopLevelMode } from "../types.js";
10
+ export interface SnapshotInputs {
11
+ mode: TopLevelMode;
12
+ currentWindowId: string;
13
+ pendingObligations: string[];
14
+ recentOutreachHashes: string[];
15
+ deniedIntents: Array<{
16
+ intentHash: string;
17
+ reason: string;
18
+ at: string;
19
+ }>;
20
+ budgets?: {
21
+ socialUsed: number;
22
+ socialLimit: number;
23
+ };
24
+ awaitingUserInput?: boolean;
25
+ riskSuppressed?: boolean;
26
+ }
27
+ /**
28
+ * Build a ContinuitySnapshot from loaded inputs.
29
+ *
30
+ * In production, inputs come from state-system (mode, budgets, obligations),
31
+ * workspace (outreach hashes, denied intents), and runtime context (window ID).
32
+ */
33
+ export declare function buildContinuitySnapshot(inputs: SnapshotInputs): ContinuitySnapshot;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Build a ContinuitySnapshot from loaded inputs.
3
+ *
4
+ * In production, inputs come from state-system (mode, budgets, obligations),
5
+ * workspace (outreach hashes, denied intents), and runtime context (window ID).
6
+ */
7
+ export function buildContinuitySnapshot(inputs) {
8
+ return {
9
+ mode: inputs.mode,
10
+ currentWindowId: inputs.currentWindowId,
11
+ pendingObligations: inputs.pendingObligations,
12
+ recentOutreachHashes: inputs.recentOutreachHashes,
13
+ deniedIntents: inputs.deniedIntents,
14
+ budgets: inputs.budgets,
15
+ awaitingUserInput: inputs.awaitingUserInput,
16
+ riskSuppressed: inputs.riskSuppressed,
17
+ };
18
+ }
@@ -26,7 +26,7 @@ export function startRuntimeService(ctx) {
26
26
  // - observability-system (event store setup)
27
27
  // - control-plane-system (heartbeat bridge preparation)
28
28
  const workspaceRoot = ctx?.workspaceRoot ?? process.cwd();
29
- const version = "0.1.3";
29
+ const version = "0.1.0";
30
30
  activeHandle = {
31
31
  ready: true,
32
32
  version,
@@ -13,7 +13,7 @@ async function buildAtmosphere(sceneContext) {
13
13
  };
14
14
  }
15
15
  async function selectImpulses(sceneContext) {
16
- if (sceneContext.sceneType === "explain") {
16
+ if (sceneContext.sceneType === "explain" || sceneContext.sceneType === "user_reply") {
17
17
  return [];
18
18
  }
19
19
  return [getImpulseTemplate(sceneContext.sceneType)];
@@ -24,6 +24,11 @@ const SCENE_POLICIES = {
24
24
  preferredTags: ["explain", "principle", "context", "truthfulness"],
25
25
  budget: { maxSnippets: 2, maxTotalCharacters: 420 },
26
26
  },
27
+ user_reply: {
28
+ sourcePriority: ["SOUL", "IDENTITY", "USER", "MEMORY"],
29
+ preferredTags: ["continuity", "tone", "conversation", "authenticity"],
30
+ budget: { maxSnippets: 2, maxTotalCharacters: 280 },
31
+ },
27
32
  };
28
33
  function tagScore(candidate, preferredTags) {
29
34
  const normalized = new Set(candidate.tags.map((tag) => tag.toLowerCase()));
@@ -1,3 +1,3 @@
1
1
  import type { AtmosphereBlock, GuidanceSceneType, ImpulseBlock } from "./types.js";
2
2
  export declare function getBaselineAtmosphereTemplate(): Pick<AtmosphereBlock, "kind" | "text" | "reviewStatus">;
3
- export declare function getImpulseTemplate(sceneType: Exclude<GuidanceSceneType, "explain">): ImpulseBlock;
3
+ export declare function getImpulseTemplate(sceneType: Exclude<GuidanceSceneType, "explain" | "user_reply">): ImpulseBlock;
@@ -1,4 +1,4 @@
1
- export type GuidanceSceneType = "social" | "reply" | "outreach" | "quiet" | "explain";
1
+ export type GuidanceSceneType = "social" | "reply" | "outreach" | "quiet" | "explain" | "user_reply";
2
2
  export type GuidanceMode = "active" | "quiet" | "maintenance_only" | "paused_for_interrupt";
3
3
  export type GuidanceRiskLevel = "low" | "medium" | "high";
4
4
  export type AtmosphereOpenness = "open" | "narrow" | "quiet";
@@ -2,7 +2,7 @@ export { createObservabilityDatabase, type ObservabilityDatabase } from "./db/in
2
2
  export * as obsSchema from "./db/schema/index.js";
3
3
  export { REDACTION_CONFIG, DEFAULT_REDACTION_POLICY, getFieldRedactionRule, type RedactionPolicy, type RedactionRule, type SensitivityLevel, } from "./redaction/policy.js";
4
4
  export { redactEvent, createEmptyManifest, mergeManifests, type RedactionManifest, type RedactionResult, } from "./redaction/manifest.js";
5
- export { DecisionLedger, type QuietLifecycleEvent, type OutreachDecision } from "./services/decision-ledger.js";
5
+ export { DecisionLedger, type QuietLifecycleEvent, type OutreachDecision, type HeartbeatDecisionEvent } from "./services/decision-ledger.js";
6
6
  export { GovernanceAudit, type CredentialLifecycleAudit } from "./services/governance-audit.js";
7
7
  export { ExecutionTelemetry, type ExecutionAttemptInput } from "./services/execution-telemetry.js";
8
8
  export { EvidenceQueryEngine, } from "./query/evidence-query-engine.js";
@@ -20,10 +20,23 @@ export interface OutreachDecision {
20
20
  messagePreview?: string;
21
21
  createdAt: string;
22
22
  }
23
+ export interface HeartbeatDecisionEvent {
24
+ id: string;
25
+ tickId: string;
26
+ traceId: string;
27
+ runtimeScope: "rhythm" | "user_task" | "user_reply";
28
+ triggerSource: "heartbeat_bridge" | "user_task" | "user_reply" | "interrupt" | "resume";
29
+ decisionStatus: "heartbeat_ok" | "intent_selected" | "denied" | "deferred";
30
+ reasons: string[];
31
+ intentId?: string;
32
+ mode: "active" | "quiet" | "maintenance_only" | "paused_for_interrupt";
33
+ createdAt: string;
34
+ }
23
35
  export declare class DecisionLedger {
24
36
  private db;
25
37
  constructor(db: ObservabilityDatabase);
26
38
  recordDecision(record: DecisionRecord): Promise<void>;
39
+ recordHeartbeatDecision(event: HeartbeatDecisionEvent): Promise<void>;
27
40
  recordQuietLifecycle(event: QuietLifecycleEvent): Promise<void>;
28
41
  recordOutreachDecision(event: OutreachDecision): Promise<void>;
29
42
  queryByTickId(tickId: string): Promise<DecisionRecord[]>;
@@ -26,6 +26,31 @@ export class DecisionLedger {
26
26
  });
27
27
  await persistRedactionManifest(this.db, redacted.id, "decision.recorded", manifest);
28
28
  }
29
+ async recordHeartbeatDecision(event) {
30
+ const { redacted, manifest } = redactEvent(event);
31
+ // Map decisionStatus to existing verdict field without changing business semantics
32
+ const verdict = mapHeartbeatStatusToVerdict(redacted.decisionStatus);
33
+ await this.db.db.insert(decisionLedger).values({
34
+ id: redacted.id,
35
+ tickId: redacted.tickId,
36
+ traceId: redacted.traceId,
37
+ intentId: redacted.intentId ?? null,
38
+ platformId: null,
39
+ verdict,
40
+ mode: redacted.mode,
41
+ reasons: JSON.stringify(redacted.reasons),
42
+ reasonCodes: JSON.stringify(["heartbeat_decision"]),
43
+ decisionBasis: "rule_only",
44
+ evidenceRefs: JSON.stringify([
45
+ `scope:${redacted.runtimeScope}`,
46
+ `trigger:${redacted.triggerSource}`,
47
+ `status:${redacted.decisionStatus}`,
48
+ ].filter(Boolean)),
49
+ modelEvalRef: null,
50
+ createdAt: redacted.createdAt,
51
+ });
52
+ await persistRedactionManifest(this.db, redacted.id, "heartbeat.decision", manifest);
53
+ }
29
54
  async recordQuietLifecycle(event) {
30
55
  const { redacted, manifest } = redactEvent(event);
31
56
  await this.db.db.insert(decisionLedger).values({
@@ -113,3 +138,20 @@ export class DecisionLedger {
113
138
  };
114
139
  }
115
140
  }
141
+ /**
142
+ * Map heartbeat decision status to existing verdict field.
143
+ * This preserves the existing DecisionRecord semantics while allowing
144
+ * heartbeat-specific status to be recoverable from evidenceRefs.
145
+ */
146
+ function mapHeartbeatStatusToVerdict(status) {
147
+ switch (status) {
148
+ case "intent_selected":
149
+ return "allow";
150
+ case "denied":
151
+ return "deny";
152
+ case "deferred":
153
+ return "defer";
154
+ case "heartbeat_ok":
155
+ return "defer";
156
+ }
157
+ }
package/index.ts DELETED
@@ -1,200 +0,0 @@
1
- import { createRequire } from "node:module";
2
-
3
- interface RegisterApi {
4
- registerService(service: { id: string; start: () => unknown }): void;
5
- registerCli(registrar: (ctx: { program: unknown }) => void, options?: { commands?: string[] }): void;
6
- registerCommand(command: {
7
- name: string;
8
- description: string;
9
- acceptsArgs?: boolean;
10
- handler: (ctx: { args?: string }) => Promise<{ text: string }> | { text: string };
11
- }): void;
12
- registerTool(tool: unknown, options?: unknown): void;
13
- }
14
-
15
- function createFallbackCommands(): Array<{
16
- name: string;
17
- description: string;
18
- execute: (input?: Record<string, unknown>) => Promise<Record<string, unknown>>;
19
- }> {
20
- const commandNames = ["status", "policy", "credential", "quiet", "report", "session", "audit", "explain"];
21
-
22
- return commandNames.map((name) => ({
23
- name,
24
- description: `Fallback command shell for ${name}`,
25
- execute: async (_input?: Record<string, unknown>) => ({
26
- ok: false,
27
- command: name,
28
- message: "Plugin loaded in packaging fallback mode; reinstall full workspace build for command runtime.",
29
- }),
30
- }));
31
- }
32
-
33
- function resolveCommandRouterSafe(): {
34
- commands: Array<{ name: string; description: string; execute: (input?: Record<string, unknown>) => Promise<Record<string, unknown>> }>;
35
- resolve(name: string): { name: string; description: string; execute: (input?: Record<string, unknown>) => Promise<Record<string, unknown>> } | undefined;
36
- } {
37
- const require = createRequire(import.meta.url);
38
-
39
- try {
40
- const mod = require("./runtime/cli/index.js") as { createCommandRouter: () => { commands: any[]; resolve: (name: string) => any } };
41
- if (mod?.createCommandRouter) {
42
- return mod.createCommandRouter();
43
- }
44
- } catch {
45
- // fall through to fallback router
46
- }
47
-
48
- const commands = createFallbackCommands();
49
- return {
50
- commands,
51
- resolve(name: string) {
52
- return commands.find((command) => command.name === name);
53
- },
54
- };
55
- }
56
-
57
- function createRuntimeService(): { id: string; start: () => unknown } {
58
- const require = createRequire(import.meta.url);
59
-
60
- try {
61
- const runtimeMod = require("./runtime/core/second-nature/runtime/service-entry.js") as {
62
- startRuntimeService: (ctx?: Record<string, unknown>) => { ready: boolean; version: string; close: () => void };
63
- };
64
-
65
- if (runtimeMod?.startRuntimeService) {
66
- const handle = runtimeMod.startRuntimeService();
67
- return {
68
- id: "second-nature-runtime",
69
- start() {
70
- return { ready: handle.ready, version: handle.version };
71
- },
72
- };
73
- }
74
- } catch {
75
- // fall through to minimal service shell
76
- }
77
-
78
- return {
79
- id: "second-nature-runtime",
80
- start() {
81
- return { ready: true, version: "0.1.3-minimal" };
82
- },
83
- };
84
- }
85
-
86
- function createLifecycleService(): { id: string; start: () => unknown } {
87
- const require = createRequire(import.meta.url);
88
-
89
- try {
90
- const lifecycleMod = require("./runtime/core/second-nature/runtime/lifecycle-service.js") as {
91
- recordRegistration: () => { registerCount: number; phase: string; lastChangedAt: number };
92
- };
93
-
94
- if (lifecycleMod?.recordRegistration) {
95
- return {
96
- id: "second-nature-lifecycle",
97
- start() {
98
- const state = lifecycleMod.recordRegistration();
99
- return { phase: state.phase, registerCount: state.registerCount };
100
- },
101
- };
102
- }
103
- } catch {
104
- // fall through to minimal lifecycle shell
105
- }
106
-
107
- let registerCount = 0;
108
- return {
109
- id: "second-nature-lifecycle",
110
- start() {
111
- registerCount += 1;
112
- return { phase: registerCount === 1 ? "loading" : "reloading", registerCount };
113
- },
114
- };
115
- }
116
-
117
- export default {
118
- id: "second-nature",
119
- name: "Second Nature",
120
- description: "Registers command/tool/service surface with load-reload lifecycle semantics.",
121
- register(api: RegisterApi) {
122
- const router = resolveCommandRouterSafe();
123
- const runtimeService = createRuntimeService();
124
- const lifecycleService = createLifecycleService();
125
-
126
- api.registerService(runtimeService);
127
- api.registerService(lifecycleService);
128
-
129
- api.registerCli(
130
- ({ program }) => {
131
- void program;
132
- },
133
- { commands: ["second-nature"] },
134
- );
135
-
136
- api.registerCommand({
137
- name: "second-nature",
138
- description: "Route Agent-facing operational commands for Second Nature.",
139
- acceptsArgs: true,
140
- handler: async (ctx: { args?: string }) => {
141
- const command = ctx.args?.trim();
142
- if (!command) {
143
- return {
144
- text: JSON.stringify({ ok: false, message: "Missing command argument." }),
145
- };
146
- }
147
-
148
- const resolved = router.resolve(command);
149
- if (!resolved) {
150
- return {
151
- text: JSON.stringify({ ok: false, command, message: "Unknown Second Nature command." }),
152
- };
153
- }
154
-
155
- const result = await resolved.execute();
156
- return {
157
- text: JSON.stringify(result),
158
- };
159
- },
160
- });
161
-
162
- api.registerTool({
163
- name: "second_nature_ops",
164
- description: "Access the Second Nature command surface through a single tool shell.",
165
- parameters: {
166
- type: "object",
167
- additionalProperties: false,
168
- properties: {
169
- command: { type: "string" },
170
- args: { type: "object", additionalProperties: true }
171
- },
172
- required: ["command"]
173
- },
174
- async execute(_id: string, params: { command: string; args?: Record<string, unknown> }) {
175
- const resolved = router.resolve(params.command);
176
- if (!resolved) {
177
- return {
178
- content: [
179
- {
180
- type: "text",
181
- text: JSON.stringify({ ok: false, message: "Unknown Second Nature command." }),
182
- },
183
- ],
184
- };
185
- }
186
-
187
- const result = await resolved.execute(params.args);
188
-
189
- return {
190
- content: [
191
- {
192
- type: "text",
193
- text: JSON.stringify(result),
194
- },
195
- ],
196
- };
197
- },
198
- });
199
- },
200
- };