@haaaiawd/second-nature 0.1.51 → 0.2.0

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 (71) hide show
  1. package/openclaw.plugin.json +29 -29
  2. package/package.json +55 -55
  3. package/runtime/cli/commands/index.js +326 -325
  4. package/runtime/cli/ops/heartbeat-surface.d.ts +84 -84
  5. package/runtime/cli/ops/heartbeat-surface.js +100 -100
  6. package/runtime/cli/ops/ops-router.js +1555 -1482
  7. package/runtime/cli/ops/workspace-heartbeat-runner.d.ts +85 -85
  8. package/runtime/cli/ops/workspace-heartbeat-runner.js +242 -242
  9. package/runtime/connectors/base/contract.d.ts +111 -111
  10. package/runtime/connectors/base/failure-taxonomy.d.ts +13 -13
  11. package/runtime/connectors/base/failure-taxonomy.js +186 -186
  12. package/runtime/connectors/base/map-life-evidence.js +137 -137
  13. package/runtime/connectors/base/policy-layer.js +202 -202
  14. package/runtime/connectors/evidence-normalizer.d.ts +45 -0
  15. package/runtime/connectors/evidence-normalizer.js +115 -0
  16. package/runtime/connectors/manifest/manifest-schema.d.ts +152 -152
  17. package/runtime/connectors/manifest/manifest-schema.js +54 -54
  18. package/runtime/connectors/services/connector-executor-adapter.d.ts +20 -20
  19. package/runtime/connectors/services/connector-executor-adapter.js +645 -645
  20. package/runtime/core/second-nature/action/action-closure-recorder.d.ts +70 -0
  21. package/runtime/core/second-nature/action/action-closure-recorder.js +184 -0
  22. package/runtime/core/second-nature/action/action-proposal-builder.d.ts +70 -0
  23. package/runtime/core/second-nature/action/action-proposal-builder.js +217 -0
  24. package/runtime/core/second-nature/action/autonomy-policy-evaluator.d.ts +43 -0
  25. package/runtime/core/second-nature/action/autonomy-policy-evaluator.js +213 -0
  26. package/runtime/core/second-nature/action/policy-bound-dispatch.d.ts +69 -0
  27. package/runtime/core/second-nature/action/policy-bound-dispatch.js +112 -0
  28. package/runtime/core/second-nature/body/tool-affordance/affordance-side-effect.d.ts +49 -0
  29. package/runtime/core/second-nature/body/tool-affordance/affordance-side-effect.js +100 -0
  30. package/runtime/core/second-nature/control-plane/accepted-projection-loader.d.ts +45 -0
  31. package/runtime/core/second-nature/control-plane/accepted-projection-loader.js +85 -0
  32. package/runtime/core/second-nature/control-plane/heartbeat-orchestrator.d.ts +38 -0
  33. package/runtime/core/second-nature/control-plane/heartbeat-orchestrator.js +165 -0
  34. package/runtime/core/second-nature/guidance/guidance-proposal-consumer.d.ts +51 -0
  35. package/runtime/core/second-nature/guidance/guidance-proposal-consumer.js +113 -0
  36. package/runtime/core/second-nature/heartbeat/goal-lifecycle-policy.d.ts +24 -24
  37. package/runtime/core/second-nature/heartbeat/goal-lifecycle-policy.js +61 -61
  38. package/runtime/core/second-nature/heartbeat/heartbeat-loop.d.ts +97 -97
  39. package/runtime/core/second-nature/heartbeat/heartbeat-loop.js +397 -397
  40. package/runtime/core/second-nature/orchestrator/platform-capability-router.js +149 -149
  41. package/runtime/core/second-nature/perception/judgment-engine.d.ts +53 -0
  42. package/runtime/core/second-nature/perception/judgment-engine.js +239 -0
  43. package/runtime/core/second-nature/perception/perception-builder.d.ts +62 -0
  44. package/runtime/core/second-nature/perception/perception-builder.js +208 -0
  45. package/runtime/core/second-nature/perception/sensitivity-classifier.d.ts +37 -0
  46. package/runtime/core/second-nature/perception/sensitivity-classifier.js +87 -0
  47. package/runtime/core/second-nature/quiet-dream/dream-consolidation-runner.d.ts +44 -0
  48. package/runtime/core/second-nature/quiet-dream/dream-consolidation-runner.js +180 -0
  49. package/runtime/core/second-nature/quiet-dream/dream-scheduler.d.ts +36 -0
  50. package/runtime/core/second-nature/quiet-dream/dream-scheduler.js +105 -0
  51. package/runtime/core/second-nature/quiet-dream/memory-projection-lifecycle.d.ts +36 -0
  52. package/runtime/core/second-nature/quiet-dream/memory-projection-lifecycle.js +151 -0
  53. package/runtime/core/second-nature/quiet-dream/quiet-daily-review-builder.d.ts +46 -0
  54. package/runtime/core/second-nature/quiet-dream/quiet-daily-review-builder.js +123 -0
  55. package/runtime/observability/causal-loop-health.d.ts +44 -0
  56. package/runtime/observability/causal-loop-health.js +118 -0
  57. package/runtime/observability/diagnostic-redaction.d.ts +43 -0
  58. package/runtime/observability/diagnostic-redaction.js +114 -0
  59. package/runtime/observability/loop-stage-event-sink.d.ts +43 -0
  60. package/runtime/observability/loop-stage-event-sink.js +148 -0
  61. package/runtime/observability/loop-status.d.ts +46 -0
  62. package/runtime/observability/loop-status.js +85 -0
  63. package/runtime/shared/types/index.js +3 -0
  64. package/runtime/shared/types/v8-contracts.d.ts +86 -0
  65. package/runtime/shared/types/v8-contracts.js +84 -0
  66. package/runtime/storage/db/schema/index.d.ts +1 -0
  67. package/runtime/storage/db/schema/index.js +1 -0
  68. package/runtime/storage/db/schema/v8-entities.d.ts +1973 -0
  69. package/runtime/storage/db/schema/v8-entities.js +160 -0
  70. package/runtime/storage/v8-state-stores.d.ts +147 -0
  71. package/runtime/storage/v8-state-stores.js +491 -0
@@ -0,0 +1,38 @@
1
+ /**
2
+ * HeartbeatOrchestrator — v8 control-plane heartbeat cycle trace writer.
3
+ *
4
+ * Core logic: Emit ordered HeartbeatCycleTrace, invoke perception/judgment
5
+ * ports, and return cycle result without making semantic action decisions.
6
+ *
7
+ * Design authority:
8
+ * - `.anws/v8/04_SYSTEM_DESIGN/control-plane-system.md`
9
+ * - `.anws/v8/04_SYSTEM_DESIGN/shared-v8-contracts.md §3`
10
+ *
11
+ * Dependencies:
12
+ * - `src/storage/v8-state-stores.js` (writeHeartbeatCycleTrace, readHeartbeatCycleTraces)
13
+ * - `src/observability/loop-stage-event-sink.js` (recordLoopStageEvent)
14
+ * - `src/core/second-nature/perception/perception-builder.js` (buildPerceptionCards)
15
+ * - `src/core/second-nature/perception/judgment-engine.js` (runAgentJudgments)
16
+ *
17
+ * Boundary:
18
+ * - Does NOT make semantic decisions about action allowability.
19
+ * - Does NOT bypass ActionPolicyDecision.
20
+ * - Degrades gracefully on DB failure or downstream unavailable.
21
+ *
22
+ * Test coverage: tests/unit/control-plane/heartbeat-cycle-trace.test.ts
23
+ */
24
+ import type { StateDatabase } from "../../../storage/db/index.js";
25
+ import type { SourceRef, DegradedOperationResult, V8ReasonCode } from "../../../shared/types/v8-contracts.js";
26
+ export interface HeartbeatOrchestrationRequest {
27
+ workspaceRoot: string;
28
+ requestedAt?: string;
29
+ trigger?: "scheduled" | "manual" | "host";
30
+ }
31
+ export interface HeartbeatOrchestrationResult {
32
+ cycleId: string;
33
+ cycleSequence: number;
34
+ closureRef?: SourceRef;
35
+ noActionReason?: V8ReasonCode;
36
+ degraded?: DegradedOperationResult;
37
+ }
38
+ export declare function runHeartbeatCycle(db: StateDatabase, request: HeartbeatOrchestrationRequest): Promise<HeartbeatOrchestrationResult | DegradedOperationResult>;
@@ -0,0 +1,165 @@
1
+ /**
2
+ * HeartbeatOrchestrator — v8 control-plane heartbeat cycle trace writer.
3
+ *
4
+ * Core logic: Emit ordered HeartbeatCycleTrace, invoke perception/judgment
5
+ * ports, and return cycle result without making semantic action decisions.
6
+ *
7
+ * Design authority:
8
+ * - `.anws/v8/04_SYSTEM_DESIGN/control-plane-system.md`
9
+ * - `.anws/v8/04_SYSTEM_DESIGN/shared-v8-contracts.md §3`
10
+ *
11
+ * Dependencies:
12
+ * - `src/storage/v8-state-stores.js` (writeHeartbeatCycleTrace, readHeartbeatCycleTraces)
13
+ * - `src/observability/loop-stage-event-sink.js` (recordLoopStageEvent)
14
+ * - `src/core/second-nature/perception/perception-builder.js` (buildPerceptionCards)
15
+ * - `src/core/second-nature/perception/judgment-engine.js` (runAgentJudgments)
16
+ *
17
+ * Boundary:
18
+ * - Does NOT make semantic decisions about action allowability.
19
+ * - Does NOT bypass ActionPolicyDecision.
20
+ * - Degrades gracefully on DB failure or downstream unavailable.
21
+ *
22
+ * Test coverage: tests/unit/control-plane/heartbeat-cycle-trace.test.ts
23
+ */
24
+ import { writeHeartbeatCycleTrace, readHeartbeatCycleTraces, } from "../../../storage/v8-state-stores.js";
25
+ import { recordLoopStageEvent } from "../../../observability/loop-stage-event-sink.js";
26
+ import { buildPerceptionCards } from "../perception/perception-builder.js";
27
+ import { runAgentJudgments } from "../perception/judgment-engine.js";
28
+ // ───────────────────────────────────────────────────────────────
29
+ // Helpers
30
+ // ───────────────────────────────────────────────────────────────
31
+ async function nextCycleSequence(db) {
32
+ const result = await readHeartbeatCycleTraces(db, 1);
33
+ if (result.degraded || result.rows.length === 0) {
34
+ return 1;
35
+ }
36
+ return (result.rows[0]?.cycleSequence ?? 0) + 1;
37
+ }
38
+ function buildCycleId(sequence, now) {
39
+ return `cyc_${now.replace(/[:.]/g, "")}_${sequence}`;
40
+ }
41
+ // ───────────────────────────────────────────────────────────────
42
+ // Public API
43
+ // ───────────────────────────────────────────────────────────────
44
+ export async function runHeartbeatCycle(db, request) {
45
+ const now = request.requestedAt ?? new Date().toISOString();
46
+ const cycleSequence = await nextCycleSequence(db);
47
+ const cycleId = buildCycleId(cycleSequence, now);
48
+ // Write cycle trace — started
49
+ const traceResult = await writeHeartbeatCycleTrace(db, {
50
+ id: cycleId,
51
+ cycleSequence,
52
+ heartbeatStartedAt: now,
53
+ inputCount: 0,
54
+ outputCount: 0,
55
+ status: "started",
56
+ sourceRefs: [
57
+ {
58
+ uri: `sn://heartbeat/${cycleId}`,
59
+ family: "audit",
60
+ id: cycleId,
61
+ redactionClass: "none",
62
+ resolveStatus: "resolvable",
63
+ },
64
+ ],
65
+ });
66
+ if ("reason" in traceResult) {
67
+ return {
68
+ status: "degraded",
69
+ reason: "state_unreadable",
70
+ ownerStage: "ingestion",
71
+ sourceRefs: [],
72
+ operatorNextAction: "Retry heartbeat after DB recovery",
73
+ retryable: true,
74
+ };
75
+ }
76
+ // Record ingestion stage started
77
+ await recordLoopStageEvent(db, {
78
+ id: `evt_${cycleId}_ingestion`,
79
+ cycleId,
80
+ cycleSequence,
81
+ stage: "ingestion",
82
+ status: "started",
83
+ occurredAt: now,
84
+ sourceRefs: [],
85
+ });
86
+ // ── Perception stage ──
87
+ const perceptionResult = await buildPerceptionCards(db, { cycleId, now });
88
+ const perceptionDegraded = "status" in perceptionResult && perceptionResult.status === "degraded"
89
+ ? perceptionResult
90
+ : null;
91
+ await recordLoopStageEvent(db, {
92
+ id: `evt_${cycleId}_perception`,
93
+ cycleId,
94
+ cycleSequence,
95
+ stage: "perception",
96
+ status: perceptionDegraded ? "failed" : "completed",
97
+ occurredAt: new Date().toISOString(),
98
+ reason: perceptionDegraded
99
+ ? perceptionResult.reason
100
+ : undefined,
101
+ sourceRefs: [],
102
+ });
103
+ if (perceptionDegraded || !("cards" in perceptionResult)) {
104
+ return {
105
+ cycleId,
106
+ cycleSequence,
107
+ degraded: perceptionDegraded
108
+ ? {
109
+ status: "degraded",
110
+ reason: perceptionResult.reason ?? "state_unreadable",
111
+ ownerStage: "perception",
112
+ sourceRefs: [],
113
+ operatorNextAction: "Retry heartbeat after perception recovery",
114
+ retryable: true,
115
+ }
116
+ : undefined,
117
+ };
118
+ }
119
+ const cards = perceptionResult.cards;
120
+ if (cards.length === 0) {
121
+ // No cards → no judgment needed
122
+ await recordLoopStageEvent(db, {
123
+ id: `evt_${cycleId}_judgment`,
124
+ cycleId,
125
+ cycleSequence,
126
+ stage: "judgment",
127
+ status: "skipped",
128
+ occurredAt: new Date().toISOString(),
129
+ reason: "evidence_batch_empty",
130
+ sourceRefs: [],
131
+ });
132
+ return {
133
+ cycleId,
134
+ cycleSequence,
135
+ noActionReason: "evidence_batch_empty",
136
+ };
137
+ }
138
+ // ── Judgment stage ──
139
+ const judgmentResult = await runAgentJudgments(db, cards.map((c) => c.id), { now });
140
+ const judgmentFailed = judgmentResult.failed.length > 0;
141
+ await recordLoopStageEvent(db, {
142
+ id: `evt_${cycleId}_judgment`,
143
+ cycleId,
144
+ cycleSequence,
145
+ stage: "judgment",
146
+ status: judgmentFailed ? "failed" : "completed",
147
+ occurredAt: new Date().toISOString(),
148
+ sourceRefs: [],
149
+ });
150
+ // Return cycle result
151
+ return {
152
+ cycleId,
153
+ cycleSequence,
154
+ closureRef: judgmentResult.succeeded.length > 0
155
+ ? {
156
+ uri: `sn://judgment/${cycleId}`,
157
+ family: "judgment",
158
+ id: cycleId,
159
+ redactionClass: "none",
160
+ resolveStatus: "resolvable",
161
+ }
162
+ : undefined,
163
+ noActionReason: judgmentResult.succeeded.length === 0 ? "proposal_no_action" : undefined,
164
+ };
165
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * GuidanceProposalConsumer — Consume policy-downgraded proposals and
3
+ * produce owner-visible draft/notify outputs.
4
+ *
5
+ * Core logic: Read an ActionPolicyDecision with downgrade, map to
6
+ * guidance request shape, and return owner-visible output without
7
+ * executing external write.
8
+ *
9
+ * Design authority:
10
+ * - `.anws/v8/04_SYSTEM_DESIGN/guidance-voice-system.md`
11
+ * - `.anws/v8/04_SYSTEM_DESIGN/action-closure-policy-system.detail.md §3.3`
12
+ *
13
+ * Dependencies:
14
+ * - `src/core/second-nature/action/autonomy-policy-evaluator.js` (ActionPolicyDecision)
15
+ * - `src/shared/types/v8-contracts.js` (PlatformNeutralActionKind)
16
+ *
17
+ * Boundary:
18
+ * - Does not generate actual text; returns guidance request envelope.
19
+ * - Does not execute connector; only produces draft/notify intent.
20
+ * - Degrades gracefully on missing decision.
21
+ *
22
+ * Test coverage: tests/unit/guidance/guidance-proposal-consumer.test.ts
23
+ */
24
+ import type { ActionPolicyDecision } from "../action/autonomy-policy-evaluator.js";
25
+ import type { ActionProposal } from "../action/action-proposal-builder.js";
26
+ import type { PlatformNeutralActionKind, SourceRef, DegradedOperationResult, V8ReasonCode } from "../../../shared/types/v8-contracts.js";
27
+ export interface GuidanceOutput {
28
+ id: string;
29
+ mode: "draft" | "notify";
30
+ textRef: SourceRef;
31
+ sourceRefs: SourceRef[];
32
+ deliveryClaim: "not_delivered";
33
+ decisionId: string;
34
+ actionKind: PlatformNeutralActionKind;
35
+ ownerVisible: boolean;
36
+ }
37
+ export type GuidanceValidationResult = {
38
+ ok: true;
39
+ } | {
40
+ ok: false;
41
+ reason: V8ReasonCode;
42
+ sourceRefs: SourceRef[];
43
+ };
44
+ export type ConsumeGuidanceProposalResult = {
45
+ ok: true;
46
+ output: GuidanceOutput;
47
+ } | {
48
+ ok: false;
49
+ degraded: DegradedOperationResult;
50
+ };
51
+ export declare function consumeGuidanceProposal(proposal: ActionProposal, decision: ActionPolicyDecision): ConsumeGuidanceProposalResult;
@@ -0,0 +1,113 @@
1
+ /**
2
+ * GuidanceProposalConsumer — Consume policy-downgraded proposals and
3
+ * produce owner-visible draft/notify outputs.
4
+ *
5
+ * Core logic: Read an ActionPolicyDecision with downgrade, map to
6
+ * guidance request shape, and return owner-visible output without
7
+ * executing external write.
8
+ *
9
+ * Design authority:
10
+ * - `.anws/v8/04_SYSTEM_DESIGN/guidance-voice-system.md`
11
+ * - `.anws/v8/04_SYSTEM_DESIGN/action-closure-policy-system.detail.md §3.3`
12
+ *
13
+ * Dependencies:
14
+ * - `src/core/second-nature/action/autonomy-policy-evaluator.js` (ActionPolicyDecision)
15
+ * - `src/shared/types/v8-contracts.js` (PlatformNeutralActionKind)
16
+ *
17
+ * Boundary:
18
+ * - Does not generate actual text; returns guidance request envelope.
19
+ * - Does not execute connector; only produces draft/notify intent.
20
+ * - Degrades gracefully on missing decision.
21
+ *
22
+ * Test coverage: tests/unit/guidance/guidance-proposal-consumer.test.ts
23
+ */
24
+ // ───────────────────────────────────────────────────────────────
25
+ // Helpers
26
+ // ───────────────────────────────────────────────────────────────
27
+ function validateSourceRefs(proposalRefs, proofRefs) {
28
+ // Proposal must have at least one source ref
29
+ if (proposalRefs.length === 0) {
30
+ return {
31
+ ok: false,
32
+ reason: "source_refs_unresolved",
33
+ sourceRefs: [],
34
+ };
35
+ }
36
+ const allRefs = [...proposalRefs, ...proofRefs];
37
+ const unresolved = allRefs.filter((r) => r.resolveStatus === "missing" || r.resolveStatus === "permission_denied");
38
+ if (unresolved.length > 0) {
39
+ return {
40
+ ok: false,
41
+ reason: "source_refs_unresolved",
42
+ sourceRefs: unresolved,
43
+ };
44
+ }
45
+ return { ok: true };
46
+ }
47
+ function buildGuidanceOutput(proposal, decision) {
48
+ const actionKind = decision.downgradedActionKind ?? proposal.actionKind;
49
+ let mode = "notify";
50
+ if (actionKind === "draft_reply" || actionKind === "auto_reply") {
51
+ mode = "draft";
52
+ }
53
+ else if (actionKind === "draft_publish" || actionKind === "auto_publish") {
54
+ mode = "draft";
55
+ }
56
+ else if (actionKind === "notify_owner") {
57
+ mode = "notify";
58
+ }
59
+ const textRef = {
60
+ uri: `sn://guidance/${proposal.id}`,
61
+ family: "action_closure",
62
+ id: proposal.id,
63
+ redactionClass: "none",
64
+ sensitivityClass: proposal.sourceRefs[0]?.sensitivityClass ?? "public_general",
65
+ resolveStatus: "resolvable",
66
+ };
67
+ return {
68
+ id: `guidance_${decision.id}_${Date.now()}`,
69
+ mode,
70
+ textRef,
71
+ sourceRefs: [...proposal.sourceRefs, ...decision.proofRefs],
72
+ deliveryClaim: "not_delivered",
73
+ decisionId: decision.id,
74
+ actionKind,
75
+ ownerVisible: true,
76
+ };
77
+ }
78
+ // ───────────────────────────────────────────────────────────────
79
+ // Public API
80
+ // ───────────────────────────────────────────────────────────────
81
+ export function consumeGuidanceProposal(proposal, decision) {
82
+ // Only consume allowed or downgraded proposals
83
+ if (decision.decision !== "downgrade" && decision.decision !== "allow") {
84
+ return {
85
+ ok: false,
86
+ degraded: {
87
+ status: "blocked",
88
+ reason: "policy_denied_high_risk",
89
+ ownerStage: "execution",
90
+ sourceRefs: decision.proofRefs,
91
+ operatorNextAction: "Review policy decision before requesting guidance",
92
+ retryable: false,
93
+ },
94
+ };
95
+ }
96
+ // Validate source refs
97
+ const validation = validateSourceRefs(proposal.sourceRefs, decision.proofRefs);
98
+ if (!validation.ok) {
99
+ return {
100
+ ok: false,
101
+ degraded: {
102
+ status: "blocked",
103
+ reason: validation.reason,
104
+ ownerStage: "execution",
105
+ sourceRefs: validation.sourceRefs,
106
+ operatorNextAction: "Fix unresolved source refs in proposal or decision proof",
107
+ retryable: true,
108
+ },
109
+ };
110
+ }
111
+ const output = buildGuidanceOutput(proposal, decision);
112
+ return { ok: true, output };
113
+ }
@@ -1,24 +1,24 @@
1
- export interface GoalTransitionRequest {
2
- goalId: string;
3
- newStatus: "completed" | "expired" | "replaced" | "paused" | "accepted";
4
- reason: string;
5
- updatedAt: string;
6
- }
7
- /** Minimal goal shape required by the lifecycle policy evaluator. */
8
- export interface EvaluableGoal {
9
- goalId: string;
10
- kind?: string;
11
- scope?: string;
12
- status: string;
13
- updatedAt?: string;
14
- expiresAt?: string;
15
- }
16
- export interface GoalLifecyclePolicyResult {
17
- activeGoals: EvaluableGoal[];
18
- transitionRequests: GoalTransitionRequest[];
19
- evaluatedAt: string;
20
- }
21
- export interface GoalLifecyclePolicy {
22
- evaluate(goals: EvaluableGoal[]): GoalLifecyclePolicyResult;
23
- }
24
- export declare function createGoalLifecyclePolicy(): GoalLifecyclePolicy;
1
+ export interface GoalTransitionRequest {
2
+ goalId: string;
3
+ newStatus: "completed" | "expired" | "replaced" | "paused" | "accepted";
4
+ reason: string;
5
+ updatedAt: string;
6
+ }
7
+ /** Minimal goal shape required by the lifecycle policy evaluator. */
8
+ export interface EvaluableGoal {
9
+ goalId: string;
10
+ kind?: string;
11
+ scope?: string;
12
+ status: string;
13
+ updatedAt?: string;
14
+ expiresAt?: string;
15
+ }
16
+ export interface GoalLifecyclePolicyResult {
17
+ activeGoals: EvaluableGoal[];
18
+ transitionRequests: GoalTransitionRequest[];
19
+ evaluatedAt: string;
20
+ }
21
+ export interface GoalLifecyclePolicy {
22
+ evaluate(goals: EvaluableGoal[]): GoalLifecyclePolicyResult;
23
+ }
24
+ export declare function createGoalLifecyclePolicy(): GoalLifecyclePolicy;
@@ -1,61 +1,61 @@
1
- export function createGoalLifecyclePolicy() {
2
- return {
3
- evaluate(goals) {
4
- const now = new Date().toISOString();
5
- const activeGoals = [];
6
- const transitionRequests = [];
7
- // Group by kind+scope to detect duplicates among accepted goals
8
- const groups = new Map();
9
- for (const goal of goals) {
10
- if (goal.status !== "accepted")
11
- continue;
12
- const key = `${goal.kind}:${goal.scope ?? "global"}`;
13
- const list = groups.get(key) ?? [];
14
- list.push(goal);
15
- groups.set(key, list);
16
- }
17
- for (const [key, list] of groups) {
18
- // Sort by updatedAt desc, keep newest as active
19
- const sorted = [...list].sort((a, b) => {
20
- const aTime = new Date(a.updatedAt ?? 0).getTime();
21
- const bTime = new Date(b.updatedAt ?? 0).getTime();
22
- if (isNaN(aTime) || isNaN(bTime))
23
- return 0;
24
- return bTime - aTime;
25
- });
26
- const [newest, ...older] = sorted;
27
- if (newest) {
28
- activeGoals.push(newest);
29
- // Older same kind+scope goals → replace
30
- for (const old of older) {
31
- transitionRequests.push({
32
- goalId: old.goalId,
33
- newStatus: "replaced",
34
- reason: `same_kind_scope_replace:${key}`,
35
- updatedAt: now,
36
- });
37
- }
38
- }
39
- }
40
- // Detect expired goals among active
41
- for (const goal of activeGoals) {
42
- if (goal.expiresAt) {
43
- const expiresTime = new Date(goal.expiresAt).getTime();
44
- if (!isNaN(expiresTime) && expiresTime < new Date(now).getTime()) {
45
- transitionRequests.push({
46
- goalId: goal.goalId,
47
- newStatus: "expired",
48
- reason: "expires_at_reached",
49
- updatedAt: now,
50
- });
51
- }
52
- }
53
- }
54
- return {
55
- activeGoals,
56
- transitionRequests,
57
- evaluatedAt: now,
58
- };
59
- },
60
- };
61
- }
1
+ export function createGoalLifecyclePolicy() {
2
+ return {
3
+ evaluate(goals) {
4
+ const now = new Date().toISOString();
5
+ const activeGoals = [];
6
+ const transitionRequests = [];
7
+ // Group by kind+scope to detect duplicates among accepted goals
8
+ const groups = new Map();
9
+ for (const goal of goals) {
10
+ if (goal.status !== "accepted")
11
+ continue;
12
+ const key = `${goal.kind}:${goal.scope ?? "global"}`;
13
+ const list = groups.get(key) ?? [];
14
+ list.push(goal);
15
+ groups.set(key, list);
16
+ }
17
+ for (const [key, list] of groups) {
18
+ // Sort by updatedAt desc, keep newest as active
19
+ const sorted = [...list].sort((a, b) => {
20
+ const aTime = new Date(a.updatedAt ?? 0).getTime();
21
+ const bTime = new Date(b.updatedAt ?? 0).getTime();
22
+ if (isNaN(aTime) || isNaN(bTime))
23
+ return 0;
24
+ return bTime - aTime;
25
+ });
26
+ const [newest, ...older] = sorted;
27
+ if (newest) {
28
+ activeGoals.push(newest);
29
+ // Older same kind+scope goals → replace
30
+ for (const old of older) {
31
+ transitionRequests.push({
32
+ goalId: old.goalId,
33
+ newStatus: "replaced",
34
+ reason: `same_kind_scope_replace:${key}`,
35
+ updatedAt: now,
36
+ });
37
+ }
38
+ }
39
+ }
40
+ // Detect expired goals among active
41
+ for (const goal of activeGoals) {
42
+ if (goal.expiresAt) {
43
+ const expiresTime = new Date(goal.expiresAt).getTime();
44
+ if (!isNaN(expiresTime) && expiresTime < new Date(now).getTime()) {
45
+ transitionRequests.push({
46
+ goalId: goal.goalId,
47
+ newStatus: "expired",
48
+ reason: "expires_at_reached",
49
+ updatedAt: now,
50
+ });
51
+ }
52
+ }
53
+ }
54
+ return {
55
+ activeGoals,
56
+ transitionRequests,
57
+ evaluatedAt: now,
58
+ };
59
+ },
60
+ };
61
+ }