opencode-wait-a-minute 0.0.0-stage → 1.1.0-rc.10

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 (207) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +318 -2
  3. package/index.js +1836 -0
  4. package/package.json +70 -4
  5. package/skills/codebase-design/SKILL.md +114 -0
  6. package/skills/design-contract/SKILL.md +27 -0
  7. package/skills/design-critique/SKILL.md +19 -0
  8. package/skills/design-system/SKILL.md +17 -0
  9. package/skills/design-system/references/image-analysis.md +14 -0
  10. package/skills/design-system/references/tokens.md +32 -0
  11. package/skills/design-system/references/ui-rules.md +14 -0
  12. package/skills/diagnosing-bugs/SKILL.md +138 -0
  13. package/skills/frontend-design/SKILL.md +36 -0
  14. package/skills/handoff/SKILL.md +16 -0
  15. package/skills/improve-codebase-architecture/SKILL.md +71 -0
  16. package/skills/linter-negative/SKILL.md +74 -0
  17. package/skills/no-slop-ui/SKILL.md +26 -0
  18. package/skills/prototype/SKILL.md +26 -0
  19. package/skills/registry.json +44087 -0
  20. package/skills/tastemaker/SKILL.md +8 -0
  21. package/skills/tastemaker/references/tastemaker.md +14 -0
  22. package/skills/tdd/SKILL.md +38 -0
  23. package/skills/visual-review/SKILL.md +103 -0
  24. package/skills/visual-review/references/review-gates.md +42 -0
  25. package/skills/visual-review/references/surface-archetypes.md +37 -0
  26. package/skills/wizard/SKILL.md +44 -0
  27. package/skills/writing-for-agents/SKILL.md +81 -0
  28. package/src/cognition/cognition-store.js +251 -0
  29. package/src/cognition/cognitive-state.js +126 -0
  30. package/src/cognition/hypothesis-manager.js +113 -0
  31. package/src/cognition/observation-engine.js +112 -0
  32. package/src/cognition/observation-provenance.js +268 -0
  33. package/src/context/active-context-boundary.js +68 -0
  34. package/src/context/assembly.js +494 -0
  35. package/src/context/assessment-rules.js +55 -0
  36. package/src/context/capsule-staleness.js +267 -0
  37. package/src/context/capsules.js +2 -0
  38. package/src/context/compare-array.js +98 -0
  39. package/src/context/compare-object.js +89 -0
  40. package/src/context/compare-values.js +42 -0
  41. package/src/context/context-assembly-contract.js +71 -0
  42. package/src/context/context-budget-manager.js +232 -0
  43. package/src/context/context-builder.js +49 -0
  44. package/src/context/context-cache.js +55 -0
  45. package/src/context/context-capture.js +243 -0
  46. package/src/context/context-classification.js +199 -0
  47. package/src/context/context-compaction.js +179 -0
  48. package/src/context/context-decision-audit.js +267 -0
  49. package/src/context/context-demotion.js +173 -0
  50. package/src/context/context-dictionary.js +106 -0
  51. package/src/context/context-drift-detection.js +225 -0
  52. package/src/context/context-evaluation.js +374 -0
  53. package/src/context/context-event-ingress.js +100 -0
  54. package/src/context/context-event-normalization.js +224 -0
  55. package/src/context/context-feedback-loop.js +216 -0
  56. package/src/context/context-freshness.js +21 -0
  57. package/src/context/context-graph-builder.js +71 -0
  58. package/src/context/context-graph.js +451 -0
  59. package/src/context/context-history-relevance.js +321 -0
  60. package/src/context/context-interception.js +145 -0
  61. package/src/context/context-invalidation-events.js +154 -0
  62. package/src/context/context-lifecycle.js +170 -0
  63. package/src/context/context-loop-prevention.js +347 -0
  64. package/src/context/context-manager.js +340 -0
  65. package/src/context/context-memory-layers.js +265 -0
  66. package/src/context/context-minimality.js +166 -0
  67. package/src/context/context-normalization.js +130 -0
  68. package/src/context/context-optimization-metrics.js +434 -0
  69. package/src/context/context-output-policy.js +135 -0
  70. package/src/context/context-pack-authority.js +157 -0
  71. package/src/context/context-promotion.js +172 -0
  72. package/src/context/context-query-contract.js +101 -0
  73. package/src/context/context-ranking.js +164 -0
  74. package/src/context/context-rate-distortion.js +261 -0
  75. package/src/context/context-recovery.js +209 -0
  76. package/src/context/context-retrieval-integration.js +60 -0
  77. package/src/context/context-retrieval.js +93 -0
  78. package/src/context/context-router.js +457 -0
  79. package/src/context/context-routing-evaluation.js +331 -0
  80. package/src/context/context-routing-observability.js +296 -0
  81. package/src/context/context-routing-shadow.js +237 -0
  82. package/src/context/context-scope.js +181 -0
  83. package/src/context/context-snapshot.js +226 -0
  84. package/src/context/context-source-registry.js +95 -0
  85. package/src/context/context-sufficiency-gate.js +120 -0
  86. package/src/context/context-sufficiency-oracle.js +167 -0
  87. package/src/context/context-telemetry.js +176 -0
  88. package/src/context/context.js +633 -0
  89. package/src/context/dependency-closure.js +246 -0
  90. package/src/context/page-fault-tracker.js +275 -0
  91. package/src/context/prompt-engine.js +74 -0
  92. package/src/context/runtime-context-graph.js +260 -0
  93. package/src/context/selection.js +2 -0
  94. package/src/context/session.js +2 -0
  95. package/src/context/sufficiency-contract.js +300 -0
  96. package/src/context/tokenizer.js +17 -0
  97. package/src/context-benchmark-router.mjs +125 -0
  98. package/src/evidence/evidence-engine.js +161 -0
  99. package/src/evidence/evidence-factory.js +3 -0
  100. package/src/evidence/evidence-freshness.js +463 -0
  101. package/src/evidence/evidence-gap.js +76 -0
  102. package/src/evidence/evidence-lineage.js +525 -0
  103. package/src/evidence/evidence.js +136 -0
  104. package/src/evidence/freshness-validation.js +187 -0
  105. package/src/evidence/lineage.js +4 -0
  106. package/src/execution/action-evaluation.js +179 -0
  107. package/src/execution/circuit-breaker.js +70 -0
  108. package/src/execution/claim-interception.js +63 -0
  109. package/src/execution/execution-engine.js +258 -0
  110. package/src/execution/execution-intent.js +69 -0
  111. package/src/execution/execution-state.js +89 -0
  112. package/src/execution/formatting.js +60 -0
  113. package/src/execution/l1-sanitize.js +16 -0
  114. package/src/execution/l2-resource-guards.js +19 -0
  115. package/src/execution/l3-invariant-checker.js +5 -0
  116. package/src/execution/l4-containment.js +6 -0
  117. package/src/execution/runtime-guards.js +106 -0
  118. package/src/execution/strategy-identity.js +162 -0
  119. package/src/execution/tool-interceptor.js +7 -0
  120. package/src/execution/tool-result-classifier.js +63 -0
  121. package/src/integration/canary-deploy.js +143 -0
  122. package/src/integration/collector.js +59 -0
  123. package/src/integration/error-recovery.js +6 -0
  124. package/src/integration/event-bus.js +66 -0
  125. package/src/integration/health-checker.js +57 -0
  126. package/src/integration/logger.js +39 -0
  127. package/src/integration/message-handler.js +611 -0
  128. package/src/integration/opencode-db.js +153 -0
  129. package/src/integration/orchestration.js +419 -0
  130. package/src/integration/part-provenance.js +65 -0
  131. package/src/integration/rate-limiter.js +55 -0
  132. package/src/integration/release-gate.js +336 -0
  133. package/src/integration/rollback-manager.js +164 -0
  134. package/src/integration/router-adapter.js +269 -0
  135. package/src/integration/telemetry.js +45 -0
  136. package/src/integration/update-checker.js +10 -0
  137. package/src/persistence/event-sourcing.js +75 -0
  138. package/src/persistence/memory.js +501 -0
  139. package/src/persistence/migration-runner.js +16 -0
  140. package/src/persistence/persistence-manager.js +39 -0
  141. package/src/persistence/transaction-log.js +157 -0
  142. package/src/policy/action-capabilities.js +116 -0
  143. package/src/policy/action-policy.js +4 -0
  144. package/src/policy/assessment-engine.js +104 -0
  145. package/src/policy/assumption-tracking.js +66 -0
  146. package/src/policy/budget-enforcer.js +7 -0
  147. package/src/policy/config-validator.js +102 -0
  148. package/src/policy/execution-assessment.js +142 -0
  149. package/src/policy/failure-classification.js +5 -0
  150. package/src/policy/feature-flags.js +65 -0
  151. package/src/policy/governance-enforcement.js +118 -0
  152. package/src/policy/locale.js +15 -0
  153. package/src/policy/multi-signer.js +151 -0
  154. package/src/policy/policy-state-machine.js +97 -0
  155. package/src/policy/ponytail-constraint.js +38 -0
  156. package/src/policy/project-inspector.js +93 -0
  157. package/src/policy/query-builder.js +14 -0
  158. package/src/policy/request-classifier.js +79 -0
  159. package/src/policy/risk-engine.js +266 -0
  160. package/src/policy/scope-enforcement.js +140 -0
  161. package/src/policy/secret-scanner.js +68 -0
  162. package/src/policy/skill-routing.js +104 -0
  163. package/src/policy/strategy-capabilities.js +211 -0
  164. package/src/policy/strategy.js +6 -0
  165. package/src/policy/uncertainty.js +117 -0
  166. package/src/shared/container.js +44 -0
  167. package/src/shared/decision-types.js +24 -0
  168. package/src/shared/simple-test.js +160 -0
  169. package/src/shared/wam-events.js +52 -0
  170. package/src/skills/engine.js +1705 -0
  171. package/src/skills/skill-registry.js +19 -0
  172. package/src/skills/skill-routing.js +340 -0
  173. package/src/state/lifecycle-manager.js +113 -0
  174. package/src/state/operational-state.js +198 -0
  175. package/src/state/projections.js +47 -0
  176. package/src/state/replay-engine.js +5 -0
  177. package/src/state/requirement-ownership.js +20 -0
  178. package/src/state/requirement-state.js +152 -0
  179. package/src/state/run-event-identity.js +172 -0
  180. package/src/state/state-engine.js +19 -0
  181. package/src/state/state-machine.js +119 -0
  182. package/src/state/state-schema.js +318 -0
  183. package/src/state/state-store.js +335 -0
  184. package/src/state/state-transition.js +84 -0
  185. package/src/state/task-dependencies.js +287 -0
  186. package/src/state/task-execution.js +217 -0
  187. package/src/state/task-lifecycle.js +59 -0
  188. package/src/state/task-runs.js +361 -0
  189. package/src/state/task-state-file.js +86 -0
  190. package/src/state/task-state.js +41 -0
  191. package/src/state/task-status.js +54 -0
  192. package/src/state/task-store.js +48 -0
  193. package/src/state/wam-state.js +148 -0
  194. package/src/verification/completion-gate-simple.js +8 -0
  195. package/src/verification/completion-gate.js +183 -0
  196. package/src/verification/contract.js +9 -0
  197. package/src/verification/contradiction-resolution.js +255 -0
  198. package/src/verification/diagnostic-engine.js +659 -0
  199. package/src/verification/false-completion-prevention.js +100 -0
  200. package/src/verification/hook-validator.js +6 -0
  201. package/src/verification/requirements.js +6 -0
  202. package/src/verification/verification-context.js +162 -0
  203. package/src/verification/verification-lifecycle.js +66 -0
  204. package/src/verification/verification-model.js +89 -0
  205. package/src/verification/verification-persistence.js +129 -0
  206. package/src/verification/verification-policy.js +127 -0
  207. package/src/verification/verification.js +415 -0
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Context Lifecycle — state machine for context items.
3
+ *
4
+ * States: CREATED, ACTIVE, COMPRESSED, STALE, INVALIDATED, ARCHIVED.
5
+ *
6
+ * Each ContextItem progresses through these states as the working
7
+ * session evolves. The lifecycle module provides:
8
+ * - state constants and transition table
9
+ * - transition() with validation against the table
10
+ * - validity checks (isUsable, isTerminal)
11
+ * - time-based staleness helper
12
+ *
13
+ * Designed to operate on top of ContextItem produced by
14
+ * context-normalization.js without coupling to specific source payloads.
15
+ */
16
+
17
+ /**
18
+ * @typedef {"CREATED"|"ACTIVE"|"COMPRESSED"|"STALE"|"INVALIDATED"|"ARCHIVED"} LifecycleState
19
+ */
20
+
21
+ /** @type {Record<LifecycleState, LifecycleState>} */
22
+ export const LIFECYCLE_STATES = Object.freeze({
23
+ CREATED: "CREATED",
24
+ ACTIVE: "ACTIVE",
25
+ COMPRESSED: "COMPRESSED",
26
+ STALE: "STALE",
27
+ INVALIDATED: "INVALIDATED",
28
+ ARCHIVED: "ARCHIVED",
29
+ });
30
+
31
+ /**
32
+ * Allowed transitions. Terminal states (INVALIDATED, ARCHIVED) have no
33
+ * outgoing edges — once archived/invalidated, the item is read-only.
34
+ * @type {Record<LifecycleState, LifecycleState[]>}
35
+ */
36
+ export const TRANSITIONS = Object.freeze({
37
+ CREATED: ["ACTIVE", "STALE", "INVALIDATED", "ARCHIVED"],
38
+ ACTIVE: ["COMPRESSED", "STALE", "INVALIDATED", "ARCHIVED"],
39
+ COMPRESSED: ["ACTIVE", "STALE", "INVALIDATED", "ARCHIVED"],
40
+ STALE: ["ACTIVE", "INVALIDATED", "ARCHIVED", "COMPRESSED"],
41
+ INVALIDATED: [],
42
+ ARCHIVED: [],
43
+ });
44
+
45
+ /** @type {LifecycleState[]} */
46
+ export const TERMINAL_STATES = Object.freeze(["INVALIDATED", "ARCHIVED"]);
47
+
48
+ /**
49
+ * Validate that a state value is a known lifecycle state.
50
+ * @param {unknown} state
51
+ * @returns {boolean}
52
+ */
53
+ export function isLifecycleState(state) {
54
+ return typeof state === "string" && state in LIFECYCLE_STATES;
55
+ }
56
+
57
+ /**
58
+ * Check whether `from -> to` is a permitted transition.
59
+ * @param {LifecycleState} from
60
+ * @param {LifecycleState} to
61
+ * @returns {boolean}
62
+ */
63
+ export function canTransition(from, to) {
64
+ if (!isLifecycleState(from) || !isLifecycleState(to)) return false;
65
+ return TRANSITIONS[from].includes(to);
66
+ }
67
+
68
+ /**
69
+ * Transition a context item from its current state to `next`.
70
+ * Returns a NEW item with updated lifecycle metadata; the original is
71
+ * not mutated. Throws if the transition is not allowed.
72
+ *
73
+ * @param {Object} item - ContextItem with `lifecycle` field
74
+ * @param {LifecycleState} next
75
+ * @param {Object} [meta] - Optional metadata to record on the transition
76
+ * (e.g. { reason, ts, actor })
77
+ * @returns {Object} Updated item
78
+ */
79
+ export function transition(item, next, meta = {}) {
80
+ if (!item || typeof item !== "object") {
81
+ throw new TypeError("item must be a non-null object");
82
+ }
83
+ if (!isLifecycleState(next)) {
84
+ throw new TypeError(`Unknown lifecycle state: ${String(next)}`);
85
+ }
86
+ const current = item.lifecycle || LIFECYCLE_STATES.CREATED;
87
+ if (!isLifecycleState(current)) {
88
+ throw new TypeError(`Item has invalid lifecycle: ${String(current)}`);
89
+ }
90
+ if (!canTransition(current, next)) {
91
+ throw new Error(`Illegal transition: ${current} -> ${next}`);
92
+ }
93
+
94
+ const history = Array.isArray(item.lifecycleHistory)
95
+ ? [...item.lifecycleHistory, { from: current, to: next, ts: Date.now(), ...meta }]
96
+ : [{ from: current, to: next, ts: Date.now(), ...meta }];
97
+
98
+ return {
99
+ ...item,
100
+ lifecycle: next,
101
+ lifecycleHistory: history,
102
+ updatedAt: Date.now(),
103
+ };
104
+ }
105
+
106
+ /**
107
+ * Decide whether `item` is in a state usable for assembly/retrieval.
108
+ * Items in COMPRESSED, STALE, INVALIDATED or ARCHIVED are excluded.
109
+ * @param {Object} item
110
+ * @returns {boolean}
111
+ */
112
+ export function isUsable(item) {
113
+ if (!item || typeof item !== "object") return false;
114
+ const state = item.lifecycle || LIFECYCLE_STATES.CREATED;
115
+ return state === LIFECYCLE_STATES.CREATED || state === LIFECYCLE_STATES.ACTIVE;
116
+ }
117
+
118
+ /**
119
+ * Check whether the state is terminal (no further transitions possible).
120
+ * @param {LifecycleState} state
121
+ * @returns {boolean}
122
+ */
123
+ export function isTerminal(state) {
124
+ return TERMINAL_STATES.includes(state);
125
+ }
126
+
127
+ /**
128
+ * Default staleness threshold per state, in milliseconds. Items older than
129
+ * (now - timestamp) > threshold are candidates for transition to STALE.
130
+ * @type {Record<LifecycleState, number>}
131
+ */
132
+ export const STALE_THRESHOLD_MS = Object.freeze({
133
+ CREATED: 60 * 60 * 1000, // 1h
134
+ ACTIVE: 24 * 60 * 60 * 1000, // 24h
135
+ COMPRESSED: 7 * 24 * 60 * 60 * 1000, // 7d
136
+ STALE: Infinity,
137
+ INVALIDATED: Infinity,
138
+ ARCHIVED: Infinity,
139
+ });
140
+
141
+ /**
142
+ * Compute the recommended next state based on age. Returns the current
143
+ * state if it is still fresh, or STALE if it has aged past threshold.
144
+ * @param {Object} item
145
+ * @param {number} [now]
146
+ * @returns {LifecycleState}
147
+ */
148
+ export function stalenessCheck(item, now = Date.now()) {
149
+ if (!item || typeof item !== "object") {
150
+ throw new TypeError("item must be a non-null object");
151
+ }
152
+ const state = item.lifecycle || LIFECYCLE_STATES.CREATED;
153
+ const ts = typeof item.timestamp === "number" ? item.timestamp : now;
154
+ const threshold = STALE_THRESHOLD_MS[state];
155
+ if (threshold === Infinity) return state;
156
+ return now - ts > threshold ? LIFECYCLE_STATES.STALE : state;
157
+ }
158
+
159
+ export default {
160
+ LIFECYCLE_STATES,
161
+ TRANSITIONS,
162
+ TERMINAL_STATES,
163
+ STALE_THRESHOLD_MS,
164
+ isLifecycleState,
165
+ canTransition,
166
+ transition,
167
+ isUsable,
168
+ isTerminal,
169
+ stalenessCheck,
170
+ };
@@ -0,0 +1,347 @@
1
+ /**
2
+ * Context Loop Prevention — Detect repetitive query/source/decision loops.
3
+ *
4
+ * Change 80: Loop detection based on identical query/sources/decision sequences.
5
+ *
6
+ * When an agent repeatedly asks for the same context, makes the same
7
+ * decisions, or follows identical reasoning chains, this module detects
8
+ * the loop and signals that intervention is needed.
9
+ *
10
+ * Detection strategies:
11
+ * 1. Exact query repetition
12
+ * 2. Source set repetition (same sources used N times)
13
+ * 3. Decision sequence repetition (same decisions in same order)
14
+ * 4. Action hash cycle detection
15
+ */
16
+
17
+ /**
18
+ * @typedef {Object} LoopEvent
19
+ * @property {string} id - Event identifier
20
+ * @property {string} type - "query" | "source" | "decision" | "action"
21
+ * @property {string} fingerprint - Hash/key for deduplication
22
+ * @property {number} timestamp - Unix ms
23
+ * @property {Object} [data] - Additional context
24
+ */
25
+
26
+ /**
27
+ * @typedef {Object} LoopDetection
28
+ * @property {boolean} loopDetected
29
+ * @property {string} type - Type of loop detected
30
+ * @property {number} occurrences - How many times the pattern repeated
31
+ * @property {number} windowStart - First occurrence timestamp
32
+ * @property {number} windowEnd - Last occurrence timestamp
33
+ * @property {string[]} items - The repeated items
34
+ * @property {number} confidence - 0-1 confidence of loop
35
+ */
36
+
37
+ /**
38
+ * @typedef {Object} LoopConfig
39
+ * @property {number} [maxEvents=1000] - Max events to track
40
+ * @property {number} [minOccurrences=3] - Min repetitions to flag loop
41
+ * @property {number} [windowMs=300000] - Time window for loop detection (5min default)
42
+ * @property {number} [maxUniqueItems=100] - Max unique fingerprints to track
43
+ */
44
+
45
+ import { hashStrategy, sameStrategy } from "../execution/strategy-identity.js";
46
+
47
+ const DEFAULT_CONFIG = {
48
+ maxEvents: 1000,
49
+ minOccurrences: 3,
50
+ windowMs: 5 * 60 * 1000, // 5 minutes
51
+ maxUniqueItems: 100,
52
+ };
53
+
54
+ /**
55
+ * Simple deterministic hash for strings.
56
+ * @param {string} str
57
+ * @returns {string}
58
+ */
59
+ function simpleHash(str) {
60
+ let hash = 0;
61
+ for (let i = 0; i < str.length; i++) {
62
+ const char = str.charCodeAt(i);
63
+ hash = (hash << 5) - hash + char;
64
+ hash = hash & hash; // Convert to 32-bit integer
65
+ }
66
+ return Math.abs(hash).toString(36);
67
+ }
68
+
69
+ /**
70
+ * Create a fingerprint for a query.
71
+ * @param {Object} query
72
+ * @returns {string}
73
+ */
74
+ function queryFingerprint(query) {
75
+ if (typeof query === "string") return simpleHash(query);
76
+ if (query && query.text) return simpleHash(query.text);
77
+ if (query && query.q) return simpleHash(query.q);
78
+ return simpleHash(JSON.stringify(query || ""));
79
+ }
80
+
81
+ class ContextLoopDetector {
82
+ /**
83
+ * @param {LoopConfig} [config={}]
84
+ */
85
+ constructor(config = {}) {
86
+ this._config = { ...DEFAULT_CONFIG, ...config };
87
+ /** @type {LoopEvent[]} */
88
+ this._events = [];
89
+ /** @type {Map<string, number[]>} */
90
+ this._occurrenceIndex = new Map(); // fingerprint -> timestamps
91
+ }
92
+
93
+ /**
94
+ * Record a query/action/decision event.
95
+ * @param {"query"|"source"|"decision"|"action"} type
96
+ * @param {Object|string} item - The item or query string
97
+ * @param {Object} [data] - Additional data
98
+ * @returns {LoopEvent}
99
+ */
100
+ record(type, item, data = {}) {
101
+ if (!type || !item) {
102
+ throw new TypeError("type and item are required");
103
+ }
104
+
105
+ const fingerprint = this._fingerprint(type, item);
106
+ const timestamp = Date.now();
107
+
108
+ const event = {
109
+ id: `loop_${simpleHash(fingerprint + timestamp)}`,
110
+ type,
111
+ fingerprint,
112
+ timestamp,
113
+ data,
114
+ };
115
+
116
+ this._events.push(event);
117
+
118
+ // Track occurrences
119
+ if (!this._occurrenceIndex.has(fingerprint)) {
120
+ this._occurrenceIndex.set(fingerprint, []);
121
+ }
122
+ const timestamps = this._occurrenceIndex.get(fingerprint);
123
+ timestamps.push(timestamp);
124
+
125
+ // Cleanup: prune old events beyond max
126
+ if (this._events.length > this._config.maxEvents) {
127
+ this._events = this._events.slice(-this._config.maxEvents);
128
+ }
129
+ if (this._occurrenceIndex.size > this._config.maxUniqueItems) {
130
+ // Remove oldest entries
131
+ const entries = [...this._occurrenceIndex.entries()].sort(
132
+ (a, b) => Math.min(...a[1]) - Math.min(...b[1])
133
+ );
134
+ for (let i = 0; i < Math.floor(this._config.maxUniqueItems / 2); i++) {
135
+ this._occurrenceIndex.delete(entries[i][0]);
136
+ }
137
+ }
138
+
139
+ return event;
140
+ }
141
+
142
+ /**
143
+ * Generate fingerprint based on event type.
144
+ */
145
+ _fingerprint(type, item) {
146
+ if (type === "query") return queryFingerprint(item);
147
+ if (type === "source") {
148
+ const src = typeof item === "string" ? item : item.id || item.name || JSON.stringify(item);
149
+ return `src:${simpleHash(src)}`;
150
+ }
151
+ if (type === "decision") {
152
+ return `dec:${simpleHash(JSON.stringify(item))}`;
153
+ }
154
+ if (type === "action") {
155
+ const act = typeof item === "string" ? item : item.type || item.name || JSON.stringify(item);
156
+ return `act:${simpleHash(act)}`;
157
+ }
158
+ return simpleHash(JSON.stringify(item));
159
+ }
160
+
161
+ /**
162
+ * Check if a fingerprint has been seen within the time window.
163
+ * @param {string} fingerprint
164
+ * @returns {LoopDetection | null}
165
+ */
166
+ check(fingerprint) {
167
+ const timestamps = this._occurrenceIndex.get(fingerprint);
168
+ if (!timestamps || timestamps.length < this._config.minOccurrences) {
169
+ return null;
170
+ }
171
+
172
+ const now = Date.now();
173
+ const windowStart = now - this._config.windowMs;
174
+ const recent = timestamps.filter((t) => t >= windowStart);
175
+
176
+ if (recent.length < this._config.minOccurrences) {
177
+ return null;
178
+ }
179
+
180
+ return {
181
+ loopDetected: true,
182
+ type: "repetition",
183
+ occurrences: recent.length,
184
+ windowStart: recent[0],
185
+ windowEnd: recent[recent.length - 1],
186
+ items: [fingerprint],
187
+ confidence: Math.min(1, recent.length / (this._config.minOccurrences + 2)),
188
+ };
189
+ }
190
+
191
+ /**
192
+ * Check for loops across all tracked fingerprints.
193
+ * @returns {LoopDetection[]}
194
+ */
195
+ checkAll() {
196
+ const loops = [];
197
+ for (const [fingerprint] of this._occurrenceIndex) {
198
+ const result = this.check(fingerprint);
199
+ if (result) {
200
+ loops.push(result);
201
+ }
202
+ }
203
+ return loops.sort((a, b) => b.occurrences - a.occurrences);
204
+ }
205
+
206
+ /**
207
+ * Record a query and immediately check for loops.
208
+ * @param {Object|string} query
209
+ * @param {Object} [data]
210
+ * @returns {LoopDetection | null}
211
+ */
212
+ recordQuery(query, data = {}) {
213
+ this.record("query", query, data);
214
+ return this.check(queryFingerprint(query));
215
+ }
216
+
217
+ /**
218
+ * Record a decision sequence and check if the same sequence repeats.
219
+ * @param {string[]} decisions - Ordered list of decision IDs
220
+ * @returns {LoopDetection | null}
221
+ */
222
+ recordDecisionSequence(decisions) {
223
+ if (!Array.isArray(decisions)) {
224
+ throw new TypeError("decisions must be an array");
225
+ }
226
+ const fingerprint = `seq:${simpleHash(decisions.join("|"))}`;
227
+ this.record("decision", decisions);
228
+ return this.check(fingerprint);
229
+ }
230
+
231
+ /**
232
+ * Get statistics about recorded events.
233
+ * @returns {Object}
234
+ */
235
+ stats() {
236
+ let mostCommon = null;
237
+ let maxCount = 0;
238
+ for (const [fp, timestamps] of this._occurrenceIndex) {
239
+ if (timestamps.length > maxCount) {
240
+ maxCount = timestamps.length;
241
+ mostCommon = fp;
242
+ }
243
+ }
244
+ return {
245
+ totalEvents: this._events.length,
246
+ uniqueFingerprints: this._occurrenceIndex.size,
247
+ mostRepeated: mostCommon ? { fingerprint: mostCommon, count: maxCount } : null,
248
+ config: this._config,
249
+ };
250
+ }
251
+
252
+ /**
253
+ * Clear all recorded data.
254
+ */
255
+ reset() {
256
+ this._events = [];
257
+ this._occurrenceIndex.clear();
258
+ }
259
+ }
260
+
261
+ /**
262
+ * Factory function for creating a loop detector (convenience).
263
+ * @param {LoopConfig} [config={}]
264
+ * @returns {ContextLoopDetector}
265
+ */
266
+ export function createLoopDetector(config = {}) {
267
+ return new ContextLoopDetector(config);
268
+ }
269
+
270
+ /**
271
+ * Quick one-shot loop check: given an array of recent items (strings),
272
+ * detect if any item appears too frequently.
273
+ * @param {string[]} items
274
+ * @param {number} [threshold=3]
275
+ * @returns {string[]} Items that repeat too often
276
+ */
277
+ export function quickLoopCheck(items, threshold = 3) {
278
+ const counts = new Map();
279
+ for (const item of items) {
280
+ const key = typeof item === "string" ? item : JSON.stringify(item);
281
+ counts.set(key, (counts.get(key) || 0) + 1);
282
+ }
283
+ const loops = [];
284
+ for (const [item, count] of counts) {
285
+ if (count >= threshold) {
286
+ loops.push(item);
287
+ }
288
+ }
289
+ return loops;
290
+ }
291
+
292
+ /**
293
+ * Strategy repetition check: given a sequence of approvedStrategy snapshots
294
+ * (in chronological order), detect whether the agent is re-running the SAME
295
+ * strategy N times despite different surface outcomes. This catches cases
296
+ * where the user / system keeps approving identical strategies without progress.
297
+ *
298
+ * Uses canonical hashing (strategy-identity.hashStrategy) so volatile fields
299
+ * (timestamps, runIds, instanceIds) do NOT affect equality.
300
+ *
301
+ * @param {Object[]} strategySnapshots - Array of approvedStrategy objects
302
+ * @param {number} [threshold=3] - Minimum repetitions to flag a strategy loop
303
+ * @returns {{ loopDetected: boolean, hash?: string, occurrences?: number, strategy?: Object }}
304
+ */
305
+ export function detectStrategyRepetition(strategySnapshots, threshold = 3) {
306
+ if (!Array.isArray(strategySnapshots) || strategySnapshots.length < threshold) {
307
+ return { loopDetected: false };
308
+ }
309
+ const counts = new Map();
310
+ for (const snapshot of strategySnapshots) {
311
+ const hash = hashStrategy(snapshot);
312
+ if (hash === null) continue;
313
+ counts.set(hash, (counts.get(hash) || 0) + 1);
314
+ }
315
+ let topHash = null;
316
+ let topCount = 0;
317
+ let topStrategy = null;
318
+ for (const [hash, count] of counts) {
319
+ if (count > topCount) {
320
+ topCount = count;
321
+ topHash = hash;
322
+ topStrategy = strategySnapshots.find((s) => hashStrategy(s) === hash) || null;
323
+ }
324
+ }
325
+ if (topCount >= threshold) {
326
+ return {
327
+ loopDetected: true,
328
+ hash: topHash,
329
+ occurrences: topCount,
330
+ strategy: topStrategy,
331
+ };
332
+ }
333
+ return { loopDetected: false };
334
+ }
335
+
336
+ /**
337
+ * Compare two approvedStrategy objects for semantic identity, ignoring
338
+ * volatile fields. Thin wrapper around strategy-identity.sameStrategy.
339
+ *
340
+ * @param {Object} a
341
+ * @param {Object} b
342
+ * @returns {boolean}
343
+ */
344
+ export function sameStrategyIdentity(a, b) {
345
+ return sameStrategy(a, b);
346
+ }
347
+ export { ContextLoopDetector };