@haaaiawd/second-nature 0.1.33 → 0.1.38

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 (39) hide show
  1. package/agent-inner-guide.md +25 -0
  2. package/index.js +1 -1
  3. package/openclaw.plugin.json +1 -1
  4. package/package.json +1 -1
  5. package/runtime/cli/commands/goal.d.ts +1 -0
  6. package/runtime/cli/commands/goal.js +1 -0
  7. package/runtime/cli/index.js +3 -3
  8. package/runtime/cli/ops/heartbeat-surface.d.ts +6 -0
  9. package/runtime/cli/ops/heartbeat-surface.js +2 -0
  10. package/runtime/cli/ops/ops-router.js +221 -92
  11. package/runtime/cli/ops/workspace-heartbeat-runner.d.ts +24 -0
  12. package/runtime/cli/ops/workspace-heartbeat-runner.js +42 -1
  13. package/runtime/connectors/base/contract.d.ts +10 -0
  14. package/runtime/connectors/base/map-life-evidence.js +5 -0
  15. package/runtime/core/second-nature/heartbeat/heartbeat-loop.d.ts +7 -1
  16. package/runtime/core/second-nature/heartbeat/heartbeat-loop.js +25 -0
  17. package/runtime/core/second-nature/heartbeat/runtime-snapshot.d.ts +5 -0
  18. package/runtime/core/second-nature/heartbeat/runtime-snapshot.js +10 -1
  19. package/runtime/core/second-nature/heartbeat/snapshot-builder.d.ts +5 -0
  20. package/runtime/core/second-nature/orchestrator/guard-layer.js +24 -1
  21. package/runtime/core/second-nature/quiet/run-source-backed-quiet.d.ts +20 -0
  22. package/runtime/core/second-nature/quiet/run-source-backed-quiet.js +32 -2
  23. package/runtime/guidance/capability-class.d.ts +38 -0
  24. package/runtime/guidance/capability-class.js +65 -0
  25. package/runtime/guidance/guidance-assembler.d.ts +2 -0
  26. package/runtime/guidance/guidance-assembler.js +16 -4
  27. package/runtime/guidance/guidance-draft-service.js +5 -5
  28. package/runtime/guidance/impulse-assembler.d.ts +71 -0
  29. package/runtime/guidance/impulse-assembler.js +103 -0
  30. package/runtime/guidance/index.d.ts +2 -0
  31. package/runtime/guidance/index.js +2 -0
  32. package/runtime/guidance/outreach-strategy-selector.d.ts +13 -0
  33. package/runtime/guidance/outreach-strategy-selector.js +2 -2
  34. package/runtime/guidance/template-registry.d.ts +15 -2
  35. package/runtime/guidance/template-registry.js +38 -1
  36. package/runtime/guidance/types.d.ts +13 -1
  37. package/runtime/storage/goal/agent-goal-store.d.ts +2 -0
  38. package/runtime/storage/goal/agent-goal-store.js +28 -1
  39. package/runtime/storage/services/tool-experience-store.js +9 -1
@@ -23,6 +23,8 @@ import type { ConnectorExecutor } from "../../../connectors/base/contract.js";
23
23
  import type { CapabilityContractRegistry } from "../../../connectors/base/manifest.js";
24
24
  import type { NarrativeStateStore } from "../../../storage/narrative/narrative-state-store.js";
25
25
  import type { NarrativeTracePayload } from "../../../observability/services/lived-experience-audit.js";
26
+ import type { ExperienceWriter } from "../body/tool-experience/experience-writer.js";
27
+ import type { QuietDreamSchedulePort } from "../quiet/run-source-backed-quiet.js";
26
28
  export interface HeartbeatDecisionTracePayload {
27
29
  scope: RuntimeScope;
28
30
  status: HeartbeatCycleStatus;
@@ -43,12 +45,14 @@ export interface HeartbeatOutreachDispatchDeps {
43
45
  /** Optional Quiet orchestration: when set, quiet/reflection allows run source-backed Quiet writer (T2.3.3). */
44
46
  export interface HeartbeatQuietWorkflowDeps {
45
47
  workspaceRoot: string;
48
+ /** v7 T-V7C.C.3: when present, a successful Quiet write auto-triggers Dream scheduling. */
49
+ dreamSchedulePort?: QuietDreamSchedulePort;
46
50
  }
47
51
  /**
48
52
  * Resolves the heartbeat outcome for a guard-allowed intent (outreach dispatch, quiet orchestration, or default).
49
53
  * Exported for unit tests (CR-M1 wiring).
50
54
  */
51
- export declare function resolveAllowedIntentResult(intent: CandidateIntent, runtime: HeartbeatRuntimeSnapshot, inputs: SnapshotInputs, signal: HeartbeatSignal, deps: Pick<HeartbeatDeps, "outreachDispatch" | "quietWorkflow" | "connectorExecutor" | "state" | "workspaceRoot">): Promise<HeartbeatCycleResult>;
55
+ export declare function resolveAllowedIntentResult(intent: CandidateIntent, runtime: HeartbeatRuntimeSnapshot, inputs: SnapshotInputs, signal: HeartbeatSignal, deps: Pick<HeartbeatDeps, "outreachDispatch" | "quietWorkflow" | "connectorExecutor" | "state" | "workspaceRoot" | "experienceWriter">): Promise<HeartbeatCycleResult>;
52
56
  export interface HeartbeatDeps {
53
57
  /** Load snapshot inputs from state-system */
54
58
  loadSnapshotInputs: () => Promise<SnapshotInputs>;
@@ -71,6 +75,8 @@ export interface HeartbeatDeps {
71
75
  workspaceRoot?: string;
72
76
  /** T2.4.1: when present, planner resolves platform-specific intents. */
73
77
  connectorRegistry?: CapabilityContractRegistry;
78
+ /** v7 T-V7C.C.2: when present, connector attempts write ToolExperience with triggerSource="heartbeat". */
79
+ experienceWriter?: ExperienceWriter;
74
80
  }
75
81
  /**
76
82
  * Ingest a heartbeat rhythm signal and drive one full decision round.
@@ -38,6 +38,8 @@ export async function resolveAllowedIntentResult(intent, runtime, inputs, signal
38
38
  day,
39
39
  userInterestSnapshot: inputs.userInterestSnapshot,
40
40
  workspaceRoot: deps.quietWorkflow.workspaceRoot,
41
+ // v7 T-V7C.C.3: pass Dream schedule port so Quiet completion triggers Dream.
42
+ dreamSchedulePort: deps.quietWorkflow.dreamSchedulePort,
41
43
  });
42
44
  return quietRun.result;
43
45
  }
@@ -64,6 +66,8 @@ export async function resolveAllowedIntentResult(intent, runtime, inputs, signal
64
66
  };
65
67
  }
66
68
  const decisionId = `decision:${intent.id}:${Date.now()}`;
69
+ // T-V7C.C.4: inject identity from EmbodiedContext into connector request (readable, no credential)
70
+ const platformHandle = runtime.identity?.platformHandles.find((h) => h.platformId === intent.platformId)?.handle;
67
71
  const result = await deps.connectorExecutor.executeEffect({
68
72
  platformId: intent.platformId,
69
73
  intent: toCapabilityIntent(intent),
@@ -71,6 +75,12 @@ export async function resolveAllowedIntentResult(intent, runtime, inputs, signal
71
75
  decisionId,
72
76
  intentId: intent.id,
73
77
  idempotencyKey: `idem:${intent.id}:${Date.now()}`,
78
+ identity: platformHandle || runtime.identity?.canonicalName
79
+ ? {
80
+ platformHandle,
81
+ canonicalName: runtime.identity?.canonicalName,
82
+ }
83
+ : undefined,
74
84
  });
75
85
  // T3.3.1: on success, map connector result to life evidence and append.
76
86
  // On failure or empty result, no evidence is fabricated — attempt audit
@@ -96,6 +106,21 @@ export async function resolveAllowedIntentResult(intent, runtime, inputs, signal
96
106
  console.warn(`[heartbeat] evidence append failed for ${intent.platformId ?? "unknown"}: ${errorMessage}`);
97
107
  }
98
108
  }
109
+ // v7 T-V7C.C.2: record ToolExperience for all connector attempts in heartbeat.
110
+ if (deps.experienceWriter) {
111
+ try {
112
+ await deps.experienceWriter.recordExperience({
113
+ connectorId: intent.platformId,
114
+ capabilityId: toCapabilityIntent(intent),
115
+ result,
116
+ triggerSource: "heartbeat",
117
+ });
118
+ }
119
+ catch (err) {
120
+ const errorMessage = err instanceof Error ? err.message : String(err);
121
+ console.warn(`[heartbeat] ToolExperience record failed for ${intent.platformId ?? "unknown"}: ${errorMessage}`);
122
+ }
123
+ }
99
124
  const base = {
100
125
  scope: "rhythm",
101
126
  status: "intent_selected",
@@ -5,6 +5,7 @@ import type { ContinuitySnapshot, ControlPlaneSourceRef } from "../types.js";
5
5
  import type { RhythmPolicy } from "../rhythm/rhythm-policy.js";
6
6
  import { type PlannerRhythmWindowSlice } from "../rhythm/planner-rhythm-window.js";
7
7
  import type { SnapshotInputs } from "./snapshot-builder.js";
8
+ import type { AffordanceMap } from "../../../shared/types/v7-entities.js";
8
9
  export interface PlannerLifeEvidenceSlice {
9
10
  evidenceRefs: ControlPlaneSourceRef[];
10
11
  platformEventCount: number;
@@ -23,6 +24,10 @@ export interface HeartbeatRuntimeSnapshot {
23
24
  hardGuards: HardGuardDeps;
24
25
  narrativeState?: import("../../../storage/narrative/narrative-state-store.js").NarrativeState;
25
26
  relationshipMemory?: import("../../../storage/relationship/relationship-memory-store.js").RelationshipMemory;
27
+ /** v7: affordance map for breaker-aware guard evaluation (T-V7C.C.2). */
28
+ affordanceMap?: AffordanceMap;
29
+ /** T-V7C.C.4: identity profile for connector request identity injection. */
30
+ identity?: import("../../../shared/types/v7-entities.js").IdentityProfile;
26
31
  }
27
32
  export declare function buildLifeEvidenceSliceFromInputs(inputs: SnapshotInputs): PlannerLifeEvidenceSlice;
28
33
  export declare function buildHardGuardDeps(continuity: ContinuitySnapshot, inputs: SnapshotInputs): HardGuardDeps;
@@ -31,5 +31,14 @@ export function buildHeartbeatRuntimeSnapshot(timestamp, inputs, continuity) {
31
31
  const rhythmWindow = buildPlannerRhythmWindow(timestamp, continuity, policy);
32
32
  const lifeEvidence = buildLifeEvidenceSliceFromInputs(inputs);
33
33
  const hardGuards = buildHardGuardDeps(continuity, inputs);
34
- return { continuity, lifeEvidence, rhythmWindow, hardGuards, narrativeState: inputs.narrativeState, relationshipMemory: inputs.relationshipMemory };
34
+ return {
35
+ continuity,
36
+ lifeEvidence,
37
+ rhythmWindow,
38
+ hardGuards,
39
+ narrativeState: inputs.narrativeState,
40
+ relationshipMemory: inputs.relationshipMemory,
41
+ affordanceMap: inputs.affordanceMap,
42
+ identity: inputs.identity,
43
+ };
35
44
  }
@@ -12,6 +12,7 @@ import type { DeliveryCapabilitySnapshot } from "../outreach/delivery-target.js"
12
12
  import type { UserInterestSnapshot } from "../../../storage/user-interest/types.js";
13
13
  import type { NarrativeState } from "../../../storage/narrative/narrative-state-store.js";
14
14
  import type { RelationshipMemory } from "../../../storage/relationship/relationship-memory-store.js";
15
+ import type { AffordanceMap, IdentityProfile } from "../../../shared/types/v7-entities.js";
15
16
  export interface SnapshotInputs {
16
17
  mode: TopLevelMode;
17
18
  currentWindowId: string;
@@ -58,6 +59,10 @@ export interface SnapshotInputs {
58
59
  narrativeState?: NarrativeState;
59
60
  /** When present, planner uses relationship memory to influence outreach timing. */
60
61
  relationshipMemory?: RelationshipMemory;
62
+ /** v7: affordance map for breaker-aware guard evaluation (T-V7C.C.2). */
63
+ affordanceMap?: AffordanceMap;
64
+ /** T-V7C.C.4: identity profile for connector request identity injection. */
65
+ identity?: IdentityProfile;
61
66
  }
62
67
  /**
63
68
  * Build a ContinuitySnapshot from loaded inputs.
@@ -42,6 +42,27 @@ export function evaluateHardGuards(intent, runtime) {
42
42
  if (!isSourceBacked(intent)) {
43
43
  reasons.push("missing_source_refs");
44
44
  }
45
+ // v7: Affordance / breaker guard (T-V7C.C.2)
46
+ if ((intent.effectClass === "connector_action" ||
47
+ intent.effectClass === "external_platform_action") &&
48
+ runtime.affordanceMap &&
49
+ intent.platformId) {
50
+ const platformItems = runtime.affordanceMap[intent.platformId] ?? [];
51
+ const match = intent.capabilityIntent
52
+ ? platformItems.find((i) => i.capabilityId === intent.capabilityIntent)
53
+ : platformItems.find((i) => i.intent === intent.summary);
54
+ if (match) {
55
+ if (match.status === "painful") {
56
+ reasons.push("connector_circuit_open");
57
+ }
58
+ else if (match.status === "unavailable") {
59
+ reasons.push("affordance_unavailable");
60
+ }
61
+ }
62
+ else {
63
+ reasons.push("affordance_unavailable");
64
+ }
65
+ }
45
66
  const key = intentFingerprint(intent);
46
67
  if (runtime.hardGuards.hasDuplicateIntent(key)) {
47
68
  reasons.push("duplicate_intent");
@@ -74,7 +95,9 @@ export function evaluateHardGuards(intent, runtime) {
74
95
  }
75
96
  const duplicate = reasons.includes("duplicate_intent");
76
97
  const cooldown = reasons.includes("outreach_cooldown");
77
- if (duplicate || cooldown) {
98
+ const circuitOpen = reasons.includes("connector_circuit_open");
99
+ const affordanceUnavailable = reasons.includes("affordance_unavailable");
100
+ if (duplicate || cooldown || circuitOpen || affordanceUnavailable) {
78
101
  return {
79
102
  verdict: "defer",
80
103
  reasons,
@@ -1,17 +1,37 @@
1
1
  /**
2
2
  * Quiet / reflection orchestration: empty evidence → empty_state; otherwise coverage-gated artifact (T2.3.3).
3
+ *
4
+ * v7 T-V7C.C.3: After a successful Quiet artifact write, if a DreamSchedulePort is provided,
5
+ * automatically trigger scheduleDream(quiet_completion). Skip reason is embedded in HeartbeatCycleResult
6
+ * reasons when the scheduler returns "skipped" (e.g. lock held).
3
7
  */
4
8
  import type { CandidateIntent } from "../types.js";
5
9
  import type { HeartbeatRuntimeSnapshot } from "../heartbeat/runtime-snapshot.js";
6
10
  import type { HeartbeatCycleResult } from "../heartbeat/signal.js";
7
11
  import { type QuietArtifactAck } from "../../../storage/quiet/quiet-artifact-writer.js";
8
12
  import type { UserInterestSnapshot } from "../../../storage/user-interest/types.js";
13
+ /**
14
+ * Minimal port for triggering Dream after Quiet completion (T-V7C.C.3).
15
+ * Kept narrow so run-source-backed-quiet does not take a hard dependency on dream-scheduler.
16
+ */
17
+ export interface QuietDreamSchedulePort {
18
+ scheduleDream(params: {
19
+ triggerKind: "quiet_completion";
20
+ runId: string;
21
+ traceId: string;
22
+ }): Promise<{
23
+ status: "started" | "skipped" | "queued";
24
+ reason?: string;
25
+ }>;
26
+ }
9
27
  export interface RunSourceBackedQuietParams {
10
28
  candidate: CandidateIntent;
11
29
  runtime: HeartbeatRuntimeSnapshot;
12
30
  day: string;
13
31
  userInterestSnapshot?: UserInterestSnapshot;
14
32
  workspaceRoot?: string;
33
+ /** v7 T-V7C.C.3: when present, a successful Quiet artifact write auto-triggers Dream scheduling. */
34
+ dreamSchedulePort?: QuietDreamSchedulePort;
15
35
  }
16
36
  export interface RunSourceBackedQuietResult {
17
37
  result: HeartbeatCycleResult;
@@ -11,8 +11,33 @@ function toGuidanceRef(r) {
11
11
  observedAt: r.observedAt,
12
12
  };
13
13
  }
14
+ /**
15
+ * v7 T-V7C.C.3: Fire-and-forget Dream schedule after successful Quiet write.
16
+ * Returns the schedule status reason string to embed in HeartbeatCycleResult reasons.
17
+ * Never throws — Dream scheduling failure must not break the Quiet cycle result.
18
+ */
19
+ async function maybeScheduleDreamAfterQuiet(dreamSchedulePort, day) {
20
+ if (!dreamSchedulePort)
21
+ return undefined;
22
+ try {
23
+ const result = await dreamSchedulePort.scheduleDream({
24
+ triggerKind: "quiet_completion",
25
+ runId: `dream:quiet_completion:${day}:${Date.now()}`,
26
+ traceId: `trace:quiet_completion:${day}:${Date.now()}`,
27
+ });
28
+ if (result.status === "skipped") {
29
+ return `quiet_dream_skip:${result.reason ?? "lock_held"}`;
30
+ }
31
+ return "quiet_dream_scheduled";
32
+ }
33
+ catch (err) {
34
+ const msg = err instanceof Error ? err.message : String(err);
35
+ console.warn(`[run-source-backed-quiet] Dream schedule failed: ${msg}`);
36
+ return `quiet_dream_schedule_error:${msg.slice(0, 60)}`;
37
+ }
38
+ }
14
39
  export async function runSourceBackedQuiet(params) {
15
- const { candidate, runtime, day, userInterestSnapshot, workspaceRoot } = params;
40
+ const { candidate, runtime, day, userInterestSnapshot, workspaceRoot, dreamSchedulePort } = params;
16
41
  const empty = isLifeEvidenceSliceEmpty(runtime.lifeEvidence);
17
42
  if (empty) {
18
43
  const input = {
@@ -117,12 +142,17 @@ export async function runSourceBackedQuiet(params) {
117
142
  const p = await persistQuietArtifactToWorkspace(workspaceRoot, ack, reportWrite);
118
143
  persistedRelativePath = p.relativePath;
119
144
  }
145
+ // v7 T-V7C.C.3: After a successful source-backed Quiet write, auto-trigger Dream scheduling.
146
+ const dreamReason = await maybeScheduleDreamAfterQuiet(dreamSchedulePort, day);
147
+ const reasons = ["quiet_artifact_written", ...gq.hints.slice(0, 2)];
148
+ if (dreamReason)
149
+ reasons.push(dreamReason);
120
150
  return {
121
151
  result: {
122
152
  scope: "rhythm",
123
153
  status: "intent_selected",
124
154
  selectedIntentId: candidate.id,
125
- reasons: ["quiet_artifact_written", ...gq.hints.slice(0, 2)],
155
+ reasons,
126
156
  },
127
157
  artifactAck: ack,
128
158
  persistedRelativePath,
@@ -0,0 +1,38 @@
1
+ /**
2
+ * capability-class.ts — T-V7C.C.4R
3
+ *
4
+ * Core logic: infer CapabilityClass from capabilityIntent string prefix.
5
+ *
6
+ * CapabilityClass is the middle axis between intentKind (why) and platform (where),
7
+ * used by ImpulseAssembler to select appropriate behavioral impulse templates.
8
+ *
9
+ * Classification rules (prefix-based, not EffectSemanticsClass — execution layer is intentionally
10
+ * kept separate from expression layer):
11
+ * feed.* → consume (browsing / reading feeds)
12
+ * notification.* → consume (reading notifications, not interacting)
13
+ * work.* → discover (research / task discovery)
14
+ * post.* → broadcast (publishing, primary expression)
15
+ * comment.* → interact (replying to others' content)
16
+ * message.* → interact (private messages / DMs)
17
+ * task.* → claim (claiming work items)
18
+ * agent.* → null (keepalive/internal — excluded from impulse system)
19
+ * unknown/custom → broadcast (safe default for unrecognized side-effect capabilities)
20
+ *
21
+ * Boundary:
22
+ * - Pure function, zero side effects.
23
+ * - Does NOT consult EffectSemanticsClass (execution-policy.ts) — those layers must not couple.
24
+ * - Custom capabilities declared by Claw without a prefix match default to "broadcast".
25
+ *
26
+ * Test coverage: tests/unit/guidance/capability-class.test.ts
27
+ */
28
+ /** The expression-layer classification of a capability. */
29
+ export type CapabilityClass = "consume" | "broadcast" | "interact" | "discover" | "claim";
30
+ /**
31
+ * Infer CapabilityClass from a capabilityIntent string.
32
+ *
33
+ * Returns null for agent.* capabilities (keepalive / internal — no impulse injection).
34
+ * Returns "broadcast" for unrecognized custom capabilities (safe default).
35
+ */
36
+ export declare function inferCapabilityClass(capabilityIntent: string): CapabilityClass | null;
37
+ /** Map from CapabilityClass to its default intentKind-style scene, for impulse lookup fallback. */
38
+ export declare const CAPABILITY_CLASS_SCENE_MAP: Readonly<Record<CapabilityClass, string>>;
@@ -0,0 +1,65 @@
1
+ /**
2
+ * capability-class.ts — T-V7C.C.4R
3
+ *
4
+ * Core logic: infer CapabilityClass from capabilityIntent string prefix.
5
+ *
6
+ * CapabilityClass is the middle axis between intentKind (why) and platform (where),
7
+ * used by ImpulseAssembler to select appropriate behavioral impulse templates.
8
+ *
9
+ * Classification rules (prefix-based, not EffectSemanticsClass — execution layer is intentionally
10
+ * kept separate from expression layer):
11
+ * feed.* → consume (browsing / reading feeds)
12
+ * notification.* → consume (reading notifications, not interacting)
13
+ * work.* → discover (research / task discovery)
14
+ * post.* → broadcast (publishing, primary expression)
15
+ * comment.* → interact (replying to others' content)
16
+ * message.* → interact (private messages / DMs)
17
+ * task.* → claim (claiming work items)
18
+ * agent.* → null (keepalive/internal — excluded from impulse system)
19
+ * unknown/custom → broadcast (safe default for unrecognized side-effect capabilities)
20
+ *
21
+ * Boundary:
22
+ * - Pure function, zero side effects.
23
+ * - Does NOT consult EffectSemanticsClass (execution-policy.ts) — those layers must not couple.
24
+ * - Custom capabilities declared by Claw without a prefix match default to "broadcast".
25
+ *
26
+ * Test coverage: tests/unit/guidance/capability-class.test.ts
27
+ */
28
+ /**
29
+ * Infer CapabilityClass from a capabilityIntent string.
30
+ *
31
+ * Returns null for agent.* capabilities (keepalive / internal — no impulse injection).
32
+ * Returns "broadcast" for unrecognized custom capabilities (safe default).
33
+ */
34
+ export function inferCapabilityClass(capabilityIntent) {
35
+ if (!capabilityIntent || typeof capabilityIntent !== "string")
36
+ return null;
37
+ const prefix = capabilityIntent.split(".")[0]?.toLowerCase() ?? "";
38
+ switch (prefix) {
39
+ case "agent":
40
+ return null; // Keepalive/internal: excluded from impulse system entirely
41
+ case "feed":
42
+ case "notification":
43
+ return "consume";
44
+ case "work":
45
+ return "discover";
46
+ case "post":
47
+ return "broadcast";
48
+ case "comment":
49
+ case "message":
50
+ return "interact";
51
+ case "task":
52
+ return "claim";
53
+ default:
54
+ // Custom or unrecognized capability — default to broadcast (outward expression)
55
+ return "broadcast";
56
+ }
57
+ }
58
+ /** Map from CapabilityClass to its default intentKind-style scene, for impulse lookup fallback. */
59
+ export const CAPABILITY_CLASS_SCENE_MAP = {
60
+ consume: "explore",
61
+ discover: "explore",
62
+ broadcast: "social",
63
+ interact: "reply",
64
+ claim: "work",
65
+ };
@@ -1,5 +1,7 @@
1
+ import { type PlatformImpulsePort } from "./impulse-assembler.js";
1
2
  import type { GuidancePayload, GuidanceUnavailable, PersonaCandidate, SceneContext } from "./types.js";
2
3
  export declare function assembleGuidance(input: {
3
4
  sceneContext: SceneContext | null | undefined;
4
5
  personaCandidates?: PersonaCandidate[];
6
+ platformImpulsePort?: PlatformImpulsePort;
5
7
  }): Promise<GuidancePayload | GuidanceUnavailable>;
@@ -1,7 +1,8 @@
1
1
  import { buildMinimalGuidanceFallback } from "./fallback.js";
2
2
  import { buildOutputGuard } from "./output-guard.js";
3
3
  import { selectPersonaSnippets } from "./persona-selection.js";
4
- import { getBaselineAtmosphereTemplate, getImpulseTemplate } from "./template-registry.js";
4
+ import { getBaselineAtmosphereTemplate } from "./template-registry.js";
5
+ import { assembleImpulse } from "./impulse-assembler.js";
5
6
  async function buildAtmosphere(sceneContext) {
6
7
  const template = getBaselineAtmosphereTemplate();
7
8
  return {
@@ -12,11 +13,21 @@ async function buildAtmosphere(sceneContext) {
12
13
  reviewStatus: template.reviewStatus,
13
14
  };
14
15
  }
15
- async function selectImpulses(sceneContext) {
16
+ /**
17
+ * Select impulses using the dual-axis capabilityClass assembler (T-V7C.C.4R).
18
+ *
19
+ * Fallback chain: platform-specific → capabilityClass preset → intentKind → []
20
+ */
21
+ async function selectImpulses(sceneContext, deps) {
16
22
  if (sceneContext.sceneType === "explain" || sceneContext.sceneType === "user_reply") {
17
23
  return [];
18
24
  }
19
- return [getImpulseTemplate(sceneContext.sceneType)];
25
+ const result = await assembleImpulse({
26
+ sceneType: sceneContext.sceneType,
27
+ capabilityIntent: sceneContext.capabilityIntent,
28
+ platformId: sceneContext.platformId,
29
+ }, deps);
30
+ return result.impulse ? [result.impulse] : [];
20
31
  }
21
32
  export async function assembleGuidance(input) {
22
33
  if (!input.sceneContext) {
@@ -26,10 +37,11 @@ export async function assembleGuidance(input) {
26
37
  };
27
38
  }
28
39
  const sceneContext = input.sceneContext;
40
+ const deps = { platformImpulsePort: input.platformImpulsePort };
29
41
  try {
30
42
  const [atmosphere, impulses] = await Promise.all([
31
43
  buildAtmosphere(sceneContext),
32
- selectImpulses(sceneContext),
44
+ selectImpulses(sceneContext, deps),
33
45
  ]);
34
46
  const personaDecision = selectPersonaSnippets({
35
47
  sceneContext,
@@ -19,16 +19,16 @@ const SCENE_KINDS = [
19
19
  "reconnect",
20
20
  ];
21
21
  function buildDraftText(request, claims) {
22
- const anchor = claims.map((c) => c.text).join("; ");
22
+ const anchor = claims.map((c) => c.text).join("");
23
23
  switch (request.sceneKind) {
24
24
  case "outreach":
25
- return `Hi there — wanted to share something that came up: ${anchor}`;
25
+ return `有件事想跟你分享,正好碰到了:${anchor}`;
26
26
  case "follow_up":
27
- return `Following up on what we talked about: ${anchor}`;
27
+ return `接着上次聊的说一下:${anchor}`;
28
28
  case "reconnect":
29
- return `It's been a while — here's what caught my attention: ${anchor}`;
29
+ return `好久不见,最近有个东西让我想到你:${anchor}`;
30
30
  default:
31
- return `Draft for ${request.sceneKind}: ${anchor}`;
31
+ return `关于 ${request.sceneKind}:${anchor}`;
32
32
  }
33
33
  }
34
34
  export async function generateGuidanceDraft(request, deps) {
@@ -0,0 +1,71 @@
1
+ /**
2
+ * impulse-assembler.ts — T-V7C.C.4R
3
+ *
4
+ * Core logic: three-level fallback impulse selection.
5
+ *
6
+ * Priority chain (highest → lowest):
7
+ * 1. platform-specific impulse — Claw-defined per platformId, loaded from workspace
8
+ * 2. capabilityClass preset — derived from capabilityIntent prefix via inferCapabilityClass
9
+ * 3. intentKind fallback — existing scene type impulse (social/outreach/reply/quiet)
10
+ * 4. null — no impulse (baseline atmosphere still applies)
11
+ *
12
+ * Exclusions:
13
+ * - agent.* capabilities → null always (keepalive/internal, not an expression action)
14
+ * - explore/work capabilityClass impulses → approved and active (T-V7C.C.4R review complete)
15
+ *
16
+ * Boundary:
17
+ * - Pure function composition; no I/O except the optional platformImpulsePort.
18
+ * - Does NOT write state or emit events.
19
+ * - SceneContext is enriched with capabilityIntent + platformId as optional fields
20
+ * to carry the dual-axis context without breaking existing SceneContext consumers.
21
+ *
22
+ * Test coverage: tests/unit/guidance/impulse-assembler.test.ts
23
+ */
24
+ import type { ImpulseBlock, GuidanceSceneType } from "./types.js";
25
+ import { type CapabilityClass } from "./capability-class.js";
26
+ /** Extended scene context carrying dual-axis impulse selection inputs. */
27
+ export interface ImpulseSelectionContext {
28
+ /** The intent kind (why) — maps to intentKind fallback impulse. */
29
+ sceneType: GuidanceSceneType;
30
+ /** The capability being executed (what physical form). e.g. "post.publish", "feed.read" */
31
+ capabilityIntent?: string;
32
+ /** The platform being targeted. Used for platform-specific impulse lookup. */
33
+ platformId?: string;
34
+ }
35
+ /**
36
+ * Port for loading platform-specific impulse overrides.
37
+ * Claw implements this by writing impulse files to the workspace.
38
+ * When absent, the assembler skips platform-specific lookup gracefully.
39
+ */
40
+ export interface PlatformImpulsePort {
41
+ /**
42
+ * Load a platform-specific impulse for the given platformId + capabilityClass.
43
+ * Returns null when no override is defined.
44
+ */
45
+ loadPlatformImpulse(input: {
46
+ platformId: string;
47
+ capabilityClass: CapabilityClass;
48
+ }): Promise<ImpulseBlock | null>;
49
+ }
50
+ export interface ImpulseAssemblerResult {
51
+ /** The selected impulse, or null if no applicable impulse exists. */
52
+ impulse: ImpulseBlock | null;
53
+ /** Which level of the fallback chain was used. */
54
+ source: "platform_specific" | "capability_class" | "intent_kind" | "none";
55
+ /** The inferred capability class (null for agent.* or unknown intent). */
56
+ capabilityClass: CapabilityClass | null;
57
+ }
58
+ /**
59
+ * Select the most specific impulse for a given scene + capability context.
60
+ *
61
+ * Fallback chain:
62
+ * platform-specific → capabilityClass preset → intentKind → null
63
+ */
64
+ export declare function assembleImpulse(ctx: ImpulseSelectionContext, deps: {
65
+ platformImpulsePort?: PlatformImpulsePort;
66
+ }): Promise<ImpulseAssemblerResult>;
67
+ /**
68
+ * Synchronous variant for contexts where capabilityClass + intentKind are sufficient
69
+ * and no platform-specific port is needed (e.g. guidance_payload ops command preview).
70
+ */
71
+ export declare function assembleImpulseSync(ctx: ImpulseSelectionContext): ImpulseAssemblerResult;
@@ -0,0 +1,103 @@
1
+ /**
2
+ * impulse-assembler.ts — T-V7C.C.4R
3
+ *
4
+ * Core logic: three-level fallback impulse selection.
5
+ *
6
+ * Priority chain (highest → lowest):
7
+ * 1. platform-specific impulse — Claw-defined per platformId, loaded from workspace
8
+ * 2. capabilityClass preset — derived from capabilityIntent prefix via inferCapabilityClass
9
+ * 3. intentKind fallback — existing scene type impulse (social/outreach/reply/quiet)
10
+ * 4. null — no impulse (baseline atmosphere still applies)
11
+ *
12
+ * Exclusions:
13
+ * - agent.* capabilities → null always (keepalive/internal, not an expression action)
14
+ * - explore/work capabilityClass impulses → approved and active (T-V7C.C.4R review complete)
15
+ *
16
+ * Boundary:
17
+ * - Pure function composition; no I/O except the optional platformImpulsePort.
18
+ * - Does NOT write state or emit events.
19
+ * - SceneContext is enriched with capabilityIntent + platformId as optional fields
20
+ * to carry the dual-axis context without breaking existing SceneContext consumers.
21
+ *
22
+ * Test coverage: tests/unit/guidance/impulse-assembler.test.ts
23
+ */
24
+ import { inferCapabilityClass, CAPABILITY_CLASS_SCENE_MAP, } from "./capability-class.js";
25
+ import { getImpulseTemplate, getCapabilityClassImpulseTemplate, } from "./template-registry.js";
26
+ // ─── Core assembly logic ──────────────────────────────────────────────────────
27
+ /**
28
+ * Select the most specific impulse for a given scene + capability context.
29
+ *
30
+ * Fallback chain:
31
+ * platform-specific → capabilityClass preset → intentKind → null
32
+ */
33
+ export async function assembleImpulse(ctx, deps) {
34
+ // Infer capability class from capabilityIntent prefix
35
+ const capabilityClass = ctx.capabilityIntent
36
+ ? inferCapabilityClass(ctx.capabilityIntent)
37
+ : null;
38
+ // agent.* → excluded entirely
39
+ if (ctx.capabilityIntent && inferCapabilityClass(ctx.capabilityIntent) === null &&
40
+ ctx.capabilityIntent.startsWith("agent.")) {
41
+ return { impulse: null, source: "none", capabilityClass: null };
42
+ }
43
+ // ── Level 1: platform-specific ──────────────────────────────────────────────
44
+ if (ctx.platformId && capabilityClass && deps.platformImpulsePort) {
45
+ try {
46
+ const platformImpulse = await deps.platformImpulsePort.loadPlatformImpulse({
47
+ platformId: ctx.platformId,
48
+ capabilityClass,
49
+ });
50
+ if (platformImpulse) {
51
+ return { impulse: platformImpulse, source: "platform_specific", capabilityClass };
52
+ }
53
+ }
54
+ catch {
55
+ // Port failure → fall through gracefully
56
+ }
57
+ }
58
+ // ── Level 2: capabilityClass preset ─────────────────────────────────────────
59
+ if (capabilityClass) {
60
+ const ccImpulseKind = CAPABILITY_CLASS_SCENE_MAP[capabilityClass];
61
+ const ccImpulse = getCapabilityClassImpulseTemplate(ccImpulseKind);
62
+ if (ccImpulse) {
63
+ return { impulse: ccImpulse, source: "capability_class", capabilityClass };
64
+ }
65
+ // explore/work are pending review → fall through to intentKind
66
+ }
67
+ // ── Level 3: intentKind fallback ─────────────────────────────────────────────
68
+ const sceneType = ctx.sceneType;
69
+ if (sceneType !== "explain" && sceneType !== "user_reply") {
70
+ const intentImpulse = getImpulseTemplate(sceneType);
71
+ return { impulse: intentImpulse, source: "intent_kind", capabilityClass };
72
+ }
73
+ // ── Level 4: no impulse ───────────────────────────────────────────────────────
74
+ return { impulse: null, source: "none", capabilityClass };
75
+ }
76
+ /**
77
+ * Synchronous variant for contexts where capabilityClass + intentKind are sufficient
78
+ * and no platform-specific port is needed (e.g. guidance_payload ops command preview).
79
+ */
80
+ export function assembleImpulseSync(ctx) {
81
+ const capabilityClass = ctx.capabilityIntent
82
+ ? inferCapabilityClass(ctx.capabilityIntent)
83
+ : null;
84
+ // agent.* excluded
85
+ if (ctx.capabilityIntent?.startsWith("agent.")) {
86
+ return { impulse: null, source: "none", capabilityClass: null };
87
+ }
88
+ // capabilityClass preset (sync — no platform port available)
89
+ if (capabilityClass) {
90
+ const ccImpulseKind = CAPABILITY_CLASS_SCENE_MAP[capabilityClass];
91
+ const ccImpulse = getCapabilityClassImpulseTemplate(ccImpulseKind);
92
+ if (ccImpulse) {
93
+ return { impulse: ccImpulse, source: "capability_class", capabilityClass };
94
+ }
95
+ }
96
+ // intentKind fallback
97
+ const sceneType = ctx.sceneType;
98
+ if (sceneType !== "explain" && sceneType !== "user_reply") {
99
+ const intentImpulse = getImpulseTemplate(sceneType);
100
+ return { impulse: intentImpulse, source: "intent_kind", capabilityClass };
101
+ }
102
+ return { impulse: null, source: "none", capabilityClass };
103
+ }
@@ -5,6 +5,8 @@ export * from "./output-guard.js";
5
5
  export * from "./fallback.js";
6
6
  export * from "./template-registry.js";
7
7
  export * from "./review-workflow.js";
8
+ export * from "./capability-class.js";
9
+ export * from "./impulse-assembler.js";
8
10
  export * from "./guidance-assembler.js";
9
11
  export * from "./outreach-draft-schema.js";
10
12
  export * from "./draft-outreach-message.js";
@@ -5,6 +5,8 @@ export * from "./output-guard.js";
5
5
  export * from "./fallback.js";
6
6
  export * from "./template-registry.js";
7
7
  export * from "./review-workflow.js";
8
+ export * from "./capability-class.js";
9
+ export * from "./impulse-assembler.js";
8
10
  export * from "./guidance-assembler.js";
9
11
  export * from "./outreach-draft-schema.js";
10
12
  export * from "./draft-outreach-message.js";