@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.
- package/index.js +151 -0
- package/openclaw.plugin.json +2 -2
- package/package.json +4 -4
- package/runtime/connectors/social-community/moltbook/api-client.d.ts +27 -0
- package/runtime/connectors/social-community/moltbook/api-client.js +71 -0
- package/runtime/connectors/social-community/moltbook/index.d.ts +1 -0
- package/runtime/connectors/social-community/moltbook/index.js +1 -0
- package/runtime/core/second-nature/guidance/user-reply-continuity.d.ts +50 -0
- package/runtime/core/second-nature/guidance/user-reply-continuity.js +80 -0
- package/runtime/core/second-nature/heartbeat/heartbeat-executor.d.ts +97 -0
- package/runtime/core/second-nature/heartbeat/heartbeat-executor.js +112 -0
- package/runtime/core/second-nature/heartbeat/heartbeat-loop.d.ts +42 -0
- package/runtime/core/second-nature/heartbeat/heartbeat-loop.js +73 -0
- package/runtime/core/second-nature/heartbeat/index.d.ts +5 -0
- package/runtime/core/second-nature/heartbeat/index.js +4 -0
- package/runtime/core/second-nature/heartbeat/scope-router.d.ts +28 -0
- package/runtime/core/second-nature/heartbeat/scope-router.js +46 -0
- package/runtime/core/second-nature/heartbeat/signal.d.ts +35 -0
- package/runtime/core/second-nature/heartbeat/signal.js +8 -0
- package/runtime/core/second-nature/heartbeat/snapshot-builder.d.ts +33 -0
- package/runtime/core/second-nature/heartbeat/snapshot-builder.js +18 -0
- package/runtime/core/second-nature/runtime/service-entry.js +1 -1
- package/runtime/guidance/guidance-assembler.js +1 -1
- package/runtime/guidance/persona-selection.js +5 -0
- package/runtime/guidance/template-registry.d.ts +1 -1
- package/runtime/guidance/types.d.ts +1 -1
- package/runtime/observability/index.d.ts +1 -1
- package/runtime/observability/services/decision-ledger.d.ts +13 -0
- package/runtime/observability/services/decision-ledger.js +42 -0
- package/index.ts +0 -200
- 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.
|
|
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
|
-
};
|