spurline-dashboard 0.3.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 (124) hide show
  1. checksums.yaml +7 -0
  2. data/lib/CLAUDE.md +11 -0
  3. data/lib/spurline/CLAUDE.md +16 -0
  4. data/lib/spurline/adapters/CLAUDE.md +12 -0
  5. data/lib/spurline/adapters/base.rb +17 -0
  6. data/lib/spurline/adapters/claude.rb +208 -0
  7. data/lib/spurline/adapters/open_ai.rb +213 -0
  8. data/lib/spurline/adapters/registry.rb +33 -0
  9. data/lib/spurline/adapters/scheduler/base.rb +15 -0
  10. data/lib/spurline/adapters/scheduler/sync.rb +15 -0
  11. data/lib/spurline/adapters/stub_adapter.rb +54 -0
  12. data/lib/spurline/agent.rb +433 -0
  13. data/lib/spurline/audit/log.rb +156 -0
  14. data/lib/spurline/audit/secret_filter.rb +121 -0
  15. data/lib/spurline/base.rb +130 -0
  16. data/lib/spurline/cartographer/CLAUDE.md +12 -0
  17. data/lib/spurline/cartographer/analyzer.rb +71 -0
  18. data/lib/spurline/cartographer/analyzers/CLAUDE.md +12 -0
  19. data/lib/spurline/cartographer/analyzers/ci_config.rb +171 -0
  20. data/lib/spurline/cartographer/analyzers/dotfiles.rb +134 -0
  21. data/lib/spurline/cartographer/analyzers/entry_points.rb +145 -0
  22. data/lib/spurline/cartographer/analyzers/file_signatures.rb +55 -0
  23. data/lib/spurline/cartographer/analyzers/manifests.rb +217 -0
  24. data/lib/spurline/cartographer/analyzers/security_scan.rb +223 -0
  25. data/lib/spurline/cartographer/repo_profile.rb +140 -0
  26. data/lib/spurline/cartographer/runner.rb +88 -0
  27. data/lib/spurline/cartographer.rb +6 -0
  28. data/lib/spurline/channels/base.rb +41 -0
  29. data/lib/spurline/channels/event.rb +136 -0
  30. data/lib/spurline/channels/github.rb +205 -0
  31. data/lib/spurline/channels/router.rb +103 -0
  32. data/lib/spurline/cli/check.rb +88 -0
  33. data/lib/spurline/cli/checks/CLAUDE.md +11 -0
  34. data/lib/spurline/cli/checks/adapter_resolution.rb +81 -0
  35. data/lib/spurline/cli/checks/agent_loadability.rb +41 -0
  36. data/lib/spurline/cli/checks/base.rb +35 -0
  37. data/lib/spurline/cli/checks/credentials.rb +43 -0
  38. data/lib/spurline/cli/checks/permissions.rb +22 -0
  39. data/lib/spurline/cli/checks/project_structure.rb +48 -0
  40. data/lib/spurline/cli/checks/session_store.rb +97 -0
  41. data/lib/spurline/cli/console.rb +73 -0
  42. data/lib/spurline/cli/credentials.rb +181 -0
  43. data/lib/spurline/cli/generators/CLAUDE.md +11 -0
  44. data/lib/spurline/cli/generators/agent.rb +123 -0
  45. data/lib/spurline/cli/generators/migration.rb +62 -0
  46. data/lib/spurline/cli/generators/project.rb +331 -0
  47. data/lib/spurline/cli/generators/tool.rb +98 -0
  48. data/lib/spurline/cli/router.rb +121 -0
  49. data/lib/spurline/configuration.rb +23 -0
  50. data/lib/spurline/dsl/CLAUDE.md +11 -0
  51. data/lib/spurline/dsl/guardrails.rb +108 -0
  52. data/lib/spurline/dsl/hooks.rb +51 -0
  53. data/lib/spurline/dsl/memory.rb +39 -0
  54. data/lib/spurline/dsl/model.rb +23 -0
  55. data/lib/spurline/dsl/persona.rb +74 -0
  56. data/lib/spurline/dsl/suspend_until.rb +53 -0
  57. data/lib/spurline/dsl/tools.rb +176 -0
  58. data/lib/spurline/errors.rb +109 -0
  59. data/lib/spurline/lifecycle/CLAUDE.md +18 -0
  60. data/lib/spurline/lifecycle/deterministic_runner.rb +207 -0
  61. data/lib/spurline/lifecycle/runner.rb +456 -0
  62. data/lib/spurline/lifecycle/states.rb +47 -0
  63. data/lib/spurline/lifecycle/suspension_boundary.rb +82 -0
  64. data/lib/spurline/memory/CLAUDE.md +12 -0
  65. data/lib/spurline/memory/context_assembler.rb +100 -0
  66. data/lib/spurline/memory/embedder/CLAUDE.md +11 -0
  67. data/lib/spurline/memory/embedder/base.rb +17 -0
  68. data/lib/spurline/memory/embedder/open_ai.rb +70 -0
  69. data/lib/spurline/memory/episode.rb +56 -0
  70. data/lib/spurline/memory/episodic_store.rb +147 -0
  71. data/lib/spurline/memory/long_term/CLAUDE.md +11 -0
  72. data/lib/spurline/memory/long_term/base.rb +22 -0
  73. data/lib/spurline/memory/long_term/postgres.rb +106 -0
  74. data/lib/spurline/memory/manager.rb +147 -0
  75. data/lib/spurline/memory/short_term.rb +57 -0
  76. data/lib/spurline/orchestration/agent_spawner.rb +151 -0
  77. data/lib/spurline/orchestration/judge.rb +109 -0
  78. data/lib/spurline/orchestration/ledger/store/base.rb +28 -0
  79. data/lib/spurline/orchestration/ledger/store/memory.rb +50 -0
  80. data/lib/spurline/orchestration/ledger.rb +339 -0
  81. data/lib/spurline/orchestration/merge_queue.rb +133 -0
  82. data/lib/spurline/orchestration/permission_intersection.rb +151 -0
  83. data/lib/spurline/orchestration/task_envelope.rb +201 -0
  84. data/lib/spurline/persona/base.rb +42 -0
  85. data/lib/spurline/persona/registry.rb +42 -0
  86. data/lib/spurline/secrets/resolver.rb +65 -0
  87. data/lib/spurline/secrets/vault.rb +42 -0
  88. data/lib/spurline/security/content.rb +76 -0
  89. data/lib/spurline/security/context_pipeline.rb +58 -0
  90. data/lib/spurline/security/gates/base.rb +36 -0
  91. data/lib/spurline/security/gates/operator_config.rb +22 -0
  92. data/lib/spurline/security/gates/system_prompt.rb +23 -0
  93. data/lib/spurline/security/gates/tool_result.rb +23 -0
  94. data/lib/spurline/security/gates/user_input.rb +22 -0
  95. data/lib/spurline/security/injection_scanner.rb +109 -0
  96. data/lib/spurline/security/pii_filter.rb +104 -0
  97. data/lib/spurline/session/CLAUDE.md +11 -0
  98. data/lib/spurline/session/resumption.rb +36 -0
  99. data/lib/spurline/session/serializer.rb +169 -0
  100. data/lib/spurline/session/session.rb +154 -0
  101. data/lib/spurline/session/store/CLAUDE.md +12 -0
  102. data/lib/spurline/session/store/base.rb +27 -0
  103. data/lib/spurline/session/store/memory.rb +45 -0
  104. data/lib/spurline/session/store/postgres.rb +123 -0
  105. data/lib/spurline/session/store/sqlite.rb +139 -0
  106. data/lib/spurline/session/suspension.rb +93 -0
  107. data/lib/spurline/session/turn.rb +98 -0
  108. data/lib/spurline/spur.rb +213 -0
  109. data/lib/spurline/streaming/CLAUDE.md +12 -0
  110. data/lib/spurline/streaming/buffer.rb +77 -0
  111. data/lib/spurline/streaming/chunk.rb +62 -0
  112. data/lib/spurline/streaming/stream_enumerator.rb +29 -0
  113. data/lib/spurline/testing.rb +245 -0
  114. data/lib/spurline/toolkit.rb +110 -0
  115. data/lib/spurline/tools/base.rb +209 -0
  116. data/lib/spurline/tools/idempotency.rb +220 -0
  117. data/lib/spurline/tools/permissions.rb +44 -0
  118. data/lib/spurline/tools/registry.rb +43 -0
  119. data/lib/spurline/tools/runner.rb +255 -0
  120. data/lib/spurline/tools/scope.rb +309 -0
  121. data/lib/spurline/tools/toolkit_registry.rb +63 -0
  122. data/lib/spurline/version.rb +5 -0
  123. data/lib/spurline.rb +56 -0
  124. metadata +218 -0
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Eagerly loaded by lib/spurline.rb and ignored by Zeitwerk.
4
+ # This is the one file that breaks the autoloading convention, because
5
+ # error classes must be available before any framework code runs and
6
+ # they define multiple constants directly under Spurline.
7
+
8
+ module Spurline
9
+ # Base error for all Spurline errors. Rescue this to catch any framework error.
10
+ class AgentError < StandardError; end
11
+
12
+ # Raised when tainted content (trust: :external or :untrusted) is converted
13
+ # to a string via #to_s. Use Content#render instead, which applies data fencing.
14
+ class TaintedContentError < AgentError; end
15
+
16
+ # Raised when the injection scanner detects a prompt injection pattern
17
+ # in content flowing through the context pipeline.
18
+ class InjectionAttemptError < AgentError; end
19
+
20
+ # Raised when the PII filter in :block mode detects personally identifiable
21
+ # information in content. Switch to :redact mode to allow content through
22
+ # with PII replaced, or :off to disable filtering.
23
+ class PIIDetectedError < AgentError; end
24
+
25
+ # Raised when a tool execution is denied by the permission system.
26
+ # Check config/permissions.yml for the tool's permission requirements.
27
+ class PermissionDeniedError < AgentError; end
28
+
29
+ # Raised when code attempts to modify a compiled persona at runtime.
30
+ # Personas are frozen on class load — define a new persona instead.
31
+ class PersonaFrozenError < AgentError; end
32
+
33
+ # Raised when an agent attempts an invalid lifecycle state transition.
34
+ # Check Spurline::Lifecycle::States for valid transitions.
35
+ class InvalidStateError < AgentError; end
36
+
37
+ # Raised when a tool call references a tool name not in the registry.
38
+ # Ensure the tool's spur gem is installed and required.
39
+ class ToolNotFoundError < AgentError; end
40
+
41
+ # Raised when the per-turn tool call limit (guardrails.max_tool_calls) is exceeded.
42
+ # Increase the limit in the agent's guardrails block or restructure the task.
43
+ class MaxToolCallsError < AgentError; end
44
+
45
+ # Raised when a tool attempts to invoke another tool. Tools are leaf nodes (ADR-003).
46
+ # Use a Spurline::Skill if you need to compose multiple tools.
47
+ class NestedToolCallError < AgentError; end
48
+
49
+ # Raised when an adapter symbol cannot be resolved in the adapter registry.
50
+ # Ensure the adapter is registered before referencing it in use_model.
51
+ class AdapterNotFoundError < AgentError; end
52
+
53
+ # Raised when the sqlite3 gem is unavailable but the SQLite session store is used.
54
+ # Add gem "sqlite3" to the application bundle when configuring :sqlite session storage.
55
+ class SQLiteUnavailableError < AgentError; end
56
+
57
+ # Raised when the pg gem is unavailable but the Postgres session store is used.
58
+ # Add gem "pg" to the application bundle when configuring :postgres session storage.
59
+ class PostgresUnavailableError < AgentError; end
60
+
61
+ # Raised when persisted session payloads cannot be decoded into Session/Turn objects.
62
+ # This indicates corrupted or incompatible serialized session data.
63
+ class SessionDeserializationError < AgentError; end
64
+
65
+ # Raised when Spurline.configure or a DSL method receives invalid configuration.
66
+ # This always fires at class load time, never at runtime.
67
+ class ConfigurationError < AgentError; end
68
+
69
+ # Raised when encrypted credentials exist but no master key can be resolved.
70
+ class CredentialsMissingKeyError < AgentError; end
71
+
72
+ # Raised when encrypted credentials cannot be decrypted (bad key or tampered file).
73
+ class CredentialsDecryptionError < AgentError; end
74
+
75
+ # Raised when a required tool secret cannot be resolved from any configured source.
76
+ class SecretNotFoundError < AgentError; end
77
+
78
+ # Raised when an embedding provider or model fails to produce a valid vector.
79
+ class EmbedderError < AgentError; end
80
+
81
+ # Raised when long-term memory persistence or retrieval fails.
82
+ class LongTermMemoryError < AgentError; end
83
+
84
+ # Raised when a session cannot be suspended from its current state.
85
+ class SuspensionError < AgentError; end
86
+
87
+ # Raised when a resume is attempted for a non-suspended session.
88
+ class InvalidResumeError < AgentError; end
89
+
90
+ # Raised when Cartographer cannot access the target repository path.
91
+ class CartographerAccessError < AgentError; end
92
+
93
+ # Raised when an individual analyzer fails to produce valid output.
94
+ class AnalyzerError < AgentError; end
95
+ class ScopeViolationError < AgentError; end
96
+ class IdempotencyKeyConflictError < AgentError; end
97
+ class PrivilegeEscalationError < AgentError; end
98
+ class LedgerError < AgentError; end
99
+ class TaskEnvelopeError < AgentError; end
100
+ class MergeConflictError < AgentError; end
101
+
102
+ # Raised when a spawned child agent fails during execution.
103
+ # The message includes parent/child session IDs for audit correlation.
104
+ class SpawnError < AgentError; end
105
+
106
+ # Raised when a toolkit name cannot be resolved in the toolkit registry.
107
+ # Ensure the toolkit class is defined and loaded before referencing it.
108
+ class ToolkitNotFoundError < AgentError; end
109
+ end
@@ -0,0 +1,18 @@
1
+ <claude-mem-context>
2
+ # Recent Activity
3
+
4
+ <!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
5
+
6
+ ### Feb 21, 2026
7
+
8
+ | ID | Time | T | Title | Read |
9
+ |----|------|---|-------|------|
10
+ | #3782 | 10:23 PM | 🔵 | Spurline session and state machine architecture analyzed for suspended sessions feature | ~700 |
11
+ | #3632 | 6:00 PM | ⚖️ | OpenAI adapter architecture with stop reason normalization and tool call accumulation | ~541 |
12
+
13
+ ### Feb 23, 2026
14
+
15
+ | ID | Time | T | Title | Read |
16
+ |----|------|---|-------|------|
17
+ | #4214 | 12:07 AM | 🔴 | Fixed message history accumulation in LLM loop | ~327 |
18
+ </claude-mem-context>
@@ -0,0 +1,207 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spurline
4
+ module Lifecycle
5
+ # Executes a fixed sequence of tools without LLM involvement.
6
+ # This is the deterministic counterpart to Lifecycle::Runner.
7
+ #
8
+ # Each tool in the sequence receives accumulated results from previous tools.
9
+ #
10
+ # Stop conditions:
11
+ # - All tools in the sequence have executed
12
+ # - max_tool_calls guardrail exceeded
13
+ # - A tool raises an error
14
+ class DeterministicRunner
15
+ def initialize(
16
+ tool_runner:,
17
+ audit_log:,
18
+ session:,
19
+ guardrails: {},
20
+ scope: nil,
21
+ idempotency_ledger: nil
22
+ )
23
+ @tool_runner = tool_runner
24
+ @audit_log = audit_log
25
+ @session = session
26
+ @guardrails = guardrails
27
+ @scope = scope
28
+ @idempotency_ledger = idempotency_ledger
29
+ end
30
+
31
+ # ASYNC-READY: executes tools sequentially, each is a blocking boundary
32
+ def run(tool_sequence:, input:, session:, &chunk_handler)
33
+ turn = session.start_turn(input: input)
34
+ @audit_log.record(:turn_start, turn: turn.number)
35
+
36
+ results = {}
37
+
38
+ tool_sequence.each_with_index do |step, idx|
39
+ tool_name, arguments = resolve_step(step, results, input)
40
+ check_max_tool_calls!(session)
41
+
42
+ filtered_arguments = redact_arguments(tool_name, arguments)
43
+ chunk_handler&.call(
44
+ Streaming::Chunk.new(
45
+ type: :tool_start,
46
+ turn: turn.number,
47
+ session_id: session.id,
48
+ metadata: { tool_name: tool_name.to_s, arguments: filtered_arguments }
49
+ )
50
+ )
51
+
52
+ started = Time.now
53
+ tool_call = { name: tool_name.to_s, arguments: arguments }
54
+ result = @tool_runner.execute(
55
+ tool_call,
56
+ session: session,
57
+ scope: @scope,
58
+ idempotency_ledger: @idempotency_ledger
59
+ )
60
+ duration_ms = ((Time.now - started) * 1000).round
61
+
62
+ @audit_log.record(
63
+ :tool_call,
64
+ tool: tool_name.to_s,
65
+ arguments: filtered_arguments,
66
+ duration_ms: duration_ms,
67
+ turn: turn.number,
68
+ step: idx + 1
69
+ )
70
+
71
+ chunk_handler&.call(
72
+ Streaming::Chunk.new(
73
+ type: :tool_end,
74
+ turn: turn.number,
75
+ session_id: session.id,
76
+ metadata: { tool_name: tool_name.to_s, duration_ms: duration_ms }
77
+ )
78
+ )
79
+
80
+ results[tool_name.to_sym] = result
81
+ end
82
+
83
+ output_text = build_output_summary(results)
84
+ output_content = Security::Gates::OperatorConfig.wrap(
85
+ output_text, key: "deterministic_result"
86
+ )
87
+ turn.finish!(output: output_content)
88
+
89
+ chunk_handler&.call(
90
+ Streaming::Chunk.new(
91
+ type: :done,
92
+ turn: turn.number,
93
+ session_id: session.id,
94
+ metadata: {
95
+ stop_reason: "deterministic_sequence_complete",
96
+ tool_count: tool_sequence.length,
97
+ }
98
+ )
99
+ )
100
+
101
+ @audit_log.record(
102
+ :turn_end,
103
+ turn: turn.number,
104
+ duration_ms: turn.duration_ms,
105
+ tool_calls: turn.tool_call_count,
106
+ mode: :deterministic
107
+ )
108
+
109
+ results
110
+ end
111
+
112
+ private
113
+
114
+ # Resolves a step definition into a tool name and arguments hash.
115
+ #
116
+ # Steps can be:
117
+ # - Symbol: tool name with default arguments (passes input through)
118
+ # - Hash with :name and :arguments (static args)
119
+ # - Hash with :name and Proc/Lambda :arguments (dynamic args)
120
+ def resolve_step(step, results_so_far, input)
121
+ case step
122
+ when Symbol
123
+ [step, { input: serialize_input(input) }]
124
+ when Hash
125
+ name = step[:name] || step[:tool]
126
+ unless name
127
+ raise Spurline::ConfigurationError,
128
+ "Deterministic sequence step must have a :name or :tool key. " \
129
+ "Got: #{step.inspect}"
130
+ end
131
+
132
+ args = step[:arguments] || step[:args]
133
+ [name.to_sym, resolve_arguments(args, results_so_far, input)]
134
+ else
135
+ raise Spurline::ConfigurationError,
136
+ "Deterministic sequence step must be a Symbol or Hash. " \
137
+ "Got: #{step.class} (#{step.inspect})"
138
+ end
139
+ end
140
+
141
+ def resolve_arguments(args, results_so_far, input)
142
+ case args
143
+ when Proc
144
+ resolved = args.call(results_so_far, input)
145
+ unless resolved.is_a?(Hash)
146
+ raise Spurline::ConfigurationError,
147
+ "Tool arguments proc/lambda must return a Hash. " \
148
+ "Got: #{resolved.class} (#{resolved.inspect})"
149
+ end
150
+ resolved
151
+ when Hash
152
+ args
153
+ when nil
154
+ { input: serialize_input(input) }
155
+ else
156
+ raise Spurline::ConfigurationError,
157
+ "Tool arguments must be a Hash, Proc/Lambda, or nil. " \
158
+ "Got: #{args.class} (#{args.inspect})"
159
+ end
160
+ end
161
+
162
+ def serialize_input(input)
163
+ if input.is_a?(Security::Content)
164
+ input.respond_to?(:render) ? input.render : input.text
165
+ else
166
+ input.to_s
167
+ end
168
+ end
169
+
170
+ def check_max_tool_calls!(session)
171
+ max = resolve_max_tool_calls
172
+ return if session.tool_call_count < max
173
+
174
+ @audit_log.record(:max_tool_calls_reached, limit: max)
175
+ raise Spurline::MaxToolCallsError,
176
+ "Tool call limit reached (#{max}). " \
177
+ "Increase max_tool_calls in the agent's guardrails block."
178
+ end
179
+
180
+ def resolve_max_tool_calls
181
+ @guardrails[:max_tool_calls] || 10
182
+ end
183
+
184
+ def redact_arguments(tool_name, arguments)
185
+ Audit::SecretFilter.filter(
186
+ arguments,
187
+ tool_name: tool_name.to_s,
188
+ registry: @tool_runner.registry
189
+ )
190
+ end
191
+
192
+ def build_output_summary(results)
193
+ results.map do |tool_name, result|
194
+ text =
195
+ if result.respond_to?(:render)
196
+ result.render
197
+ elsif result.respond_to?(:text)
198
+ result.text.to_s
199
+ else
200
+ result.inspect
201
+ end
202
+ "#{tool_name}: #{text[0..200]}"
203
+ end.join("\n")
204
+ end
205
+ end
206
+ end
207
+ end