fosm-rails-coding-agent 0.0.1
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.
- checksums.yaml +7 -0
- data/AGENTS.md +77 -0
- data/CHANGELOG.md +16 -0
- data/LICENSE +127 -0
- data/README.md +135 -0
- data/bin/fosm-coding-agent +14 -0
- data/lib/fosm_rails_coding_agent/acp_agent.rb +132 -0
- data/lib/fosm_rails_coding_agent/configuration.rb +27 -0
- data/lib/fosm_rails_coding_agent/exceptions_middleware.rb +67 -0
- data/lib/fosm_rails_coding_agent/middleware.rb +115 -0
- data/lib/fosm_rails_coding_agent/quiet_middleware.rb +18 -0
- data/lib/fosm_rails_coding_agent/railtie.rb +51 -0
- data/lib/fosm_rails_coding_agent/tools/base.rb +10 -0
- data/lib/fosm_rails_coding_agent/tools/execute_sql.rb +44 -0
- data/lib/fosm_rails_coding_agent/tools/fosm/available_events.rb +46 -0
- data/lib/fosm_rails_coding_agent/tools/fosm/fire_event.rb +46 -0
- data/lib/fosm_rails_coding_agent/tools/fosm/inspect_lifecycle.rb +44 -0
- data/lib/fosm_rails_coding_agent/tools/fosm/list_lifecycles.rb +45 -0
- data/lib/fosm_rails_coding_agent/tools/fosm/model_resolver.rb +25 -0
- data/lib/fosm_rails_coding_agent/tools/fosm/transition_history.rb +56 -0
- data/lib/fosm_rails_coding_agent/tools/fosm/why_blocked.rb +36 -0
- data/lib/fosm_rails_coding_agent/tools/get_docs.rb +58 -0
- data/lib/fosm_rails_coding_agent/tools/get_logs.rb +66 -0
- data/lib/fosm_rails_coding_agent/tools/get_models.rb +35 -0
- data/lib/fosm_rails_coding_agent/tools/get_source_location.rb +69 -0
- data/lib/fosm_rails_coding_agent/tools/project_eval.rb +76 -0
- data/lib/fosm_rails_coding_agent/transport.rb +84 -0
- data/lib/fosm_rails_coding_agent/version.rb +5 -0
- data/lib/fosm_rails_coding_agent.rb +26 -0
- metadata +139 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: a86e08a22e13276222b43f30f800f409ce3bfe8452c2ef9c51a7d3c1d8379b60
|
|
4
|
+
data.tar.gz: 8a0272687e4df3142c3cf229709ee10af544ac7d325822a5d9cc87bad936bc7a
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 470c6fdb093d724f40f96aaabdd021842ba448c40b4a2b269544237cdc58fedac0355cedab53199b76b4b1c689bb89c64c308dc6b84903ad1742912c04bfd33b
|
|
7
|
+
data.tar.gz: fcd04b3eb16daf0116d5c7ef806daa701d0fd82422ca6fe8014f1ddb091f8cc27e0439f8734cac52fe40264810fa2d191779d42cd166667933689b685ebb7bb0
|
data/AGENTS.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# Agents Guide — fosm-rails-coding-agent
|
|
2
|
+
|
|
3
|
+
Instructions for AI coding agents contributing to this codebase.
|
|
4
|
+
|
|
5
|
+
## Project Structure
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
lib/fosm_rails_coding_agent.rb # Main entrypoint, FosmRailsCodingAgent.fosm_available?
|
|
9
|
+
lib/fosm_rails_coding_agent/
|
|
10
|
+
version.rb # Gem version
|
|
11
|
+
configuration.rb # Configuration class (MCP + ACP settings)
|
|
12
|
+
transport.rb # MCP Streamable HTTP transport
|
|
13
|
+
middleware.rb # Rack middleware, tool registration
|
|
14
|
+
quiet_middleware.rb # Silence request log noise
|
|
15
|
+
exceptions_middleware.rb # Inject exception context for agents
|
|
16
|
+
railtie.rb # Rails integration
|
|
17
|
+
acp_agent.rb # ACP agent (AgentClientProtocol)
|
|
18
|
+
tools/
|
|
19
|
+
base.rb # Base class < FastMcp::Tool
|
|
20
|
+
execute_sql.rb # SQL query tool
|
|
21
|
+
get_logs.rb # Log tail tool
|
|
22
|
+
project_eval.rb # Ruby eval tool
|
|
23
|
+
get_models.rb # ActiveRecord model lister
|
|
24
|
+
get_source_location.rb # Source location finder
|
|
25
|
+
get_docs.rb # Documentation extractor
|
|
26
|
+
fosm/ # FOSM-aware tools (conditional)
|
|
27
|
+
model_resolver.rb # Shared model name resolution
|
|
28
|
+
list_lifecycles.rb # List all FOSM lifecycles
|
|
29
|
+
inspect_lifecycle.rb # Deep lifecycle introspection
|
|
30
|
+
fire_event.rb # Fire lifecycle events
|
|
31
|
+
transition_history.rb # Audit trail
|
|
32
|
+
available_events.rb # Current available events
|
|
33
|
+
why_blocked.rb # Diagnostic: why can't event fire
|
|
34
|
+
bin/
|
|
35
|
+
fosm-coding-agent # ACP agent executable (stdio)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Code Conventions
|
|
39
|
+
|
|
40
|
+
- `frozen_string_literal: true` on every `.rb` file
|
|
41
|
+
- One class per file, clear single responsibility
|
|
42
|
+
- Ruby 3.1+ features where they improve clarity
|
|
43
|
+
- Comment block on every class explaining what it does
|
|
44
|
+
- No trailing whitespace
|
|
45
|
+
- Tools follow the FastMcp::Tool DSL: `tool_name`, `description`, `arguments do`, `def call`
|
|
46
|
+
|
|
47
|
+
## Key Design Decisions
|
|
48
|
+
|
|
49
|
+
1. **FOSM tools are conditional** — only registered when `FosmRailsCodingAgent.fosm_available?` is true (checks `defined?(Fosm::Lifecycle)`)
|
|
50
|
+
2. **All tools inherit from `FosmRailsCodingAgent::Tools::Base`** which inherits from `FastMcp::Tool`
|
|
51
|
+
3. **FOSM tools live under `FosmRailsCodingAgent::Tools::Fosm` namespace** — the middleware filters by module ancestry
|
|
52
|
+
4. **ACP agent pushes context proactively** — the `new_session` hook sends FOSM lifecycle summaries
|
|
53
|
+
5. **Development only** — the Railtie raises if `config.enable_reloading` is false
|
|
54
|
+
6. **ModelResolver mixin** — shared across all FOSM tools to resolve model names (direct, Fosm:: prefix, or Registry lookup)
|
|
55
|
+
|
|
56
|
+
## Adding a New Tool
|
|
57
|
+
|
|
58
|
+
1. Create `lib/fosm_rails_coding_agent/tools/your_tool.rb`
|
|
59
|
+
2. Inherit from `FosmRailsCodingAgent::Tools::Base`
|
|
60
|
+
3. Define `tool_name`, `description`, `arguments`, and `def call`
|
|
61
|
+
4. The tool auto-registers via `Base.descendants` in the middleware
|
|
62
|
+
|
|
63
|
+
For FOSM-specific tools, place under `lib/fosm_rails_coding_agent/tools/fosm/` and namespace under `FosmRailsCodingAgent::Tools::Fosm`.
|
|
64
|
+
|
|
65
|
+
## Testing
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
bundle exec rake spec
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Dependencies
|
|
72
|
+
|
|
73
|
+
- `rails >= 7.1` — framework integration
|
|
74
|
+
- `fast-mcp ~> 1.6` — MCP server protocol
|
|
75
|
+
- `rack >= 2.0` — middleware
|
|
76
|
+
- `acp_ruby ~> 0.1` — Agent Client Protocol
|
|
77
|
+
- `fosm-rails` — optional, for FOSM lifecycle introspection
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to fosm-rails-coding-agent will be documented in this file.
|
|
4
|
+
|
|
5
|
+
## [0.0.1] - 2026-03-29
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Initial release
|
|
10
|
+
- MCP server with core tools: execute_sql, get_logs, project_eval, get_models, get_source_location, get_docs
|
|
11
|
+
- Conditional FOSM tools: fosm_list_lifecycles, fosm_inspect_lifecycle, fosm_fire_event, fosm_transition_history, fosm_available_events, fosm_why_blocked
|
|
12
|
+
- ACP agent with proactive FOSM context push on session start
|
|
13
|
+
- Rack middleware with localhost-only access control
|
|
14
|
+
- Rails Railtie for automatic development environment setup
|
|
15
|
+
- Exception interception middleware for agent-readable error context
|
|
16
|
+
- Quiet middleware to suppress request logging
|
data/LICENSE
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# Functional Source License, Version 1.1, Apache 2.0 Future License
|
|
2
|
+
|
|
3
|
+
## Abbreviation
|
|
4
|
+
|
|
5
|
+
FSL-1.1-Apache-2.0
|
|
6
|
+
|
|
7
|
+
## Notice
|
|
8
|
+
|
|
9
|
+
Copyright 2026 Abhishek Parolkar and INLOOP.STUDIO PTE LTD
|
|
10
|
+
|
|
11
|
+
## Change Date
|
|
12
|
+
|
|
13
|
+
Three years from the release date of each version of the Software and Content.
|
|
14
|
+
|
|
15
|
+
## Terms and Conditions
|
|
16
|
+
|
|
17
|
+
### Licensor ("We")
|
|
18
|
+
|
|
19
|
+
Abhishek Parolkar, INLOOP.STUDIO PTE LTD, and their affiliated entities, the party offering the Software and Content under these Terms and Conditions.
|
|
20
|
+
|
|
21
|
+
### Succession of Rights
|
|
22
|
+
|
|
23
|
+
All rights, title and interest of Abhishek Parolkar ("Original Author") under
|
|
24
|
+
this License — including but not limited to copyright ownership, the right to
|
|
25
|
+
grant licenses, the right to enforce these Terms and Conditions, and the right
|
|
26
|
+
to receive any royalties or consideration arising from the Software and
|
|
27
|
+
Content — shall, upon the Original Author's death or permanent incapacity,
|
|
28
|
+
pass to and vest in the Original Author's next-of-kin as determined by
|
|
29
|
+
applicable law of succession. The next-of-kin shall hold and may exercise all
|
|
30
|
+
such rights to the same extent as the Original Author, including the right to
|
|
31
|
+
modify the terms of future releases.
|
|
32
|
+
|
|
33
|
+
### The Software and Content
|
|
34
|
+
|
|
35
|
+
The "Software and Content" refers to each version of the software, documentation, approaches, strategies, methodologies, and all other materials that we make available under these Terms and Conditions, as indicated by our inclusion of these Terms and Conditions with the Software and Content.
|
|
36
|
+
|
|
37
|
+
### License Grant
|
|
38
|
+
|
|
39
|
+
Subject to your compliance with this License Grant and the Patents,
|
|
40
|
+
Redistribution and Trademark clauses below, we hereby grant you the right to
|
|
41
|
+
use, copy, modify, create derivative works, publicly perform, publicly display
|
|
42
|
+
and redistribute the Software and Content for any Permitted Purpose identified below.
|
|
43
|
+
|
|
44
|
+
### Permitted Purpose
|
|
45
|
+
|
|
46
|
+
A Permitted Purpose is any purpose other than a Competing Use. A Competing Use
|
|
47
|
+
means making the Software and Content available to others in a commercial product or
|
|
48
|
+
service that:
|
|
49
|
+
|
|
50
|
+
1. substitutes for the Software and Content;
|
|
51
|
+
|
|
52
|
+
2. substitutes for any other product or service we offer using the Software and Content
|
|
53
|
+
that exists as of the date we make the Software and Content available; or
|
|
54
|
+
|
|
55
|
+
3. offers the same or substantially similar functionality as the Software and Content,
|
|
56
|
+
including making the Software and Content available as a hosted or managed service
|
|
57
|
+
(e.g., software-as-a-service, platform-as-a-service, or any other on-demand service)
|
|
58
|
+
where third parties access the functionality of the Software and Content.
|
|
59
|
+
|
|
60
|
+
Permitted Purposes specifically include using the Software and Content:
|
|
61
|
+
|
|
62
|
+
1. for your internal use and access;
|
|
63
|
+
|
|
64
|
+
2. for non-commercial education;
|
|
65
|
+
|
|
66
|
+
3. for non-commercial research; and
|
|
67
|
+
|
|
68
|
+
4. in connection with professional services that you provide to a licensee
|
|
69
|
+
using the Software and Content in accordance with these Terms and Conditions.
|
|
70
|
+
|
|
71
|
+
### Patents
|
|
72
|
+
|
|
73
|
+
To the extent your use for a Permitted Purpose would necessarily infringe our
|
|
74
|
+
patents, the license grant above includes a license under our patents. If you
|
|
75
|
+
make a claim against any party that the Software and Content infringes or contributes to
|
|
76
|
+
the infringement of any patent, then your patent license to the Software and Content ends
|
|
77
|
+
immediately.
|
|
78
|
+
|
|
79
|
+
### Redistribution
|
|
80
|
+
|
|
81
|
+
The Terms and Conditions apply to all copies, modifications and derivatives of
|
|
82
|
+
the Software and Content.
|
|
83
|
+
|
|
84
|
+
If you redistribute any copies, modifications or derivatives of the Software and Content,
|
|
85
|
+
you must include a copy of or a link to these Terms and Conditions and not
|
|
86
|
+
remove any copyright notices provided in or with the Software and Content.
|
|
87
|
+
|
|
88
|
+
### Disclaimer
|
|
89
|
+
|
|
90
|
+
THE SOFTWARE AND CONTENT IS PROVIDED "AS IS" AND WITHOUT WARRANTIES OF ANY KIND, EXPRESS OR
|
|
91
|
+
IMPLIED, INCLUDING WITHOUT LIMITATION WARRANTIES OF FITNESS FOR A PARTICULAR
|
|
92
|
+
PURPOSE, MERCHANTABILITY, TITLE OR NON-INFRINGEMENT.
|
|
93
|
+
|
|
94
|
+
IN NO EVENT WILL WE HAVE ANY LIABILITY TO YOU ARISING OUT OF OR RELATED TO THE
|
|
95
|
+
SOFTWARE AND CONTENT, INCLUDING INDIRECT, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES,
|
|
96
|
+
EVEN IF WE HAVE BEEN INFORMED OF THEIR POSSIBILITY IN ADVANCE.
|
|
97
|
+
|
|
98
|
+
### Trademarks and Domain Names
|
|
99
|
+
|
|
100
|
+
Except for displaying the License Details and identifying us as the origin of
|
|
101
|
+
the Software and Content, you have no right under these Terms and Conditions to use our
|
|
102
|
+
trademarks, trade names, service marks or product names.
|
|
103
|
+
The name "FOSM-RAILS" is exclusive
|
|
104
|
+
property of Abhishek Parolkar with unlimited license to INLOOP.STUDIO PTE LTD. No right or license is granted
|
|
105
|
+
to use these names or domains in any manner whatsoever without the express written
|
|
106
|
+
permission of Abhishek Parolkar. Any unauthorized use of these names or domains is
|
|
107
|
+
strictly prohibited.
|
|
108
|
+
|
|
109
|
+
## Grant of Future License
|
|
110
|
+
|
|
111
|
+
We hereby irrevocably grant you an additional license to use the Software and Content under
|
|
112
|
+
the Apache License, Version 2.0 that is effective on the Change Date (as defined above —
|
|
113
|
+
three years from the release date of each version of the Software and Content).
|
|
114
|
+
On or after the Change Date, you may use the Software and Content under the Apache
|
|
115
|
+
License, Version 2.0, in which case the following will apply:
|
|
116
|
+
|
|
117
|
+
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
|
|
118
|
+
this file except in compliance with the License.
|
|
119
|
+
|
|
120
|
+
You may obtain a copy of the License at
|
|
121
|
+
|
|
122
|
+
[http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0)
|
|
123
|
+
|
|
124
|
+
Unless required by applicable law or agreed to in writing, software distributed
|
|
125
|
+
under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
|
126
|
+
CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
|
127
|
+
specific language governing permissions and limitations under the License.
|
data/README.md
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# fosm-rails-coding-agent
|
|
2
|
+
|
|
3
|
+
**FOSM-aware runtime intelligence for Rails — MCP server + ACP agent for coding agents.**
|
|
4
|
+
|
|
5
|
+
Gives coding agents (Claude Code, Codex, Copilot, OpenCode) runtime access to your Rails application: database queries, logs, code evaluation, and — when [fosm-rails](https://github.com/inloopstudio/fosm-rails) is present — deep introspection of FOSM lifecycle state machines, transitions, guards, and audit trails.
|
|
6
|
+
|
|
7
|
+
## Architecture: MCP + ACP
|
|
8
|
+
|
|
9
|
+
This gem exposes two complementary protocols:
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
┌─────────────────────────────────────────────────┐
|
|
13
|
+
│ Rails Application │
|
|
14
|
+
│ │
|
|
15
|
+
│ ┌──────────┐ ┌───────────────────────────┐ │
|
|
16
|
+
│ │ fosm-rails│ │ fosm-rails-coding-agent │ │
|
|
17
|
+
│ │ lifecycles│◄──►│ │ │
|
|
18
|
+
│ └──────────┘ │ ┌─────────┐ ┌──────────┐ │ │
|
|
19
|
+
│ │ │MCP Tools│ │ACP Agent │ │ │
|
|
20
|
+
│ │ │(pull) │ │(push) │ │ │
|
|
21
|
+
│ │ └────┬────┘ └─────┬─────┘ │ │
|
|
22
|
+
│ └───────┼────────────┼───────┘ │
|
|
23
|
+
└──────────────────────────┼────────────┼──────────┘
|
|
24
|
+
│ │
|
|
25
|
+
┌──────┴────────────┴───────┐
|
|
26
|
+
│ Claude Code / Codex / │
|
|
27
|
+
│ Copilot / OpenCode │
|
|
28
|
+
└───────────────────────────┘
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### MCP Server (pull-based)
|
|
32
|
+
|
|
33
|
+
An MCP server embedded as Rack middleware at `/fosm-agent/mcp`. Coding agents query it on demand via standard MCP tool calls. Built on [fast-mcp](https://github.com/yjacquin/fast-mcp).
|
|
34
|
+
|
|
35
|
+
### ACP Agent (push-based)
|
|
36
|
+
|
|
37
|
+
An ACP agent (`bin/fosm-coding-agent`) that implements the [Agent Client Protocol](https://agentclientprotocol.com/). When a session starts, it proactively pushes FOSM lifecycle context — state definitions, events, guards, recent transitions — so the coding agent starts with full situational awareness.
|
|
38
|
+
|
|
39
|
+
The most powerful developer tooling doesn't wait for agents to discover context through trial and error — it **pushes** the right context at the right time. When your coding agent opens a PR that touches an `Invoice` model, it should already know that `Invoice` has a lifecycle with states `draft → sent → paid`, a guard requiring line items before sending, and that the last three transitions were all `pay` events. MCP lets the agent pull when it needs to; ACP lets the application push when it knows the agent should care.
|
|
40
|
+
|
|
41
|
+
Inspired by the thinking in [The future of coding agents is vertical integration](https://tidewave.ai/blog/the-future-of-coding-agents-is-vertical-integration).
|
|
42
|
+
|
|
43
|
+
## Installation
|
|
44
|
+
|
|
45
|
+
Add to your Gemfile:
|
|
46
|
+
|
|
47
|
+
```ruby
|
|
48
|
+
gem "fosm-rails-coding-agent"
|
|
49
|
+
|
|
50
|
+
# Optional: for FOSM lifecycle introspection
|
|
51
|
+
gem "fosm-rails"
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Run:
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
bundle install
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Auto-activates in development via its Railtie. No configuration needed for basic use.
|
|
61
|
+
|
|
62
|
+
## Configuration
|
|
63
|
+
|
|
64
|
+
In `config/environments/development.rb`:
|
|
65
|
+
|
|
66
|
+
```ruby
|
|
67
|
+
config.fosm_coding_agent.allow_remote_access = false # default: localhost only
|
|
68
|
+
config.fosm_coding_agent.sql_row_limit = 50 # max rows from execute_sql
|
|
69
|
+
config.fosm_coding_agent.eval_timeout_ms = 30_000 # project_eval timeout
|
|
70
|
+
config.fosm_coding_agent.log_tail_default = 100 # default log lines
|
|
71
|
+
config.fosm_coding_agent.acp_enabled = true # enable ACP agent
|
|
72
|
+
config.fosm_coding_agent.acp_push_fosm_context = true # push FOSM context on session start
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## MCP Tools
|
|
76
|
+
|
|
77
|
+
### Core Tools (always available)
|
|
78
|
+
|
|
79
|
+
| Tool | Description |
|
|
80
|
+
|------|-------------|
|
|
81
|
+
| `execute_sql` | Run SQL queries against the app database (50 row limit) |
|
|
82
|
+
| `get_logs` | Tail the Rails log with optional regex filter |
|
|
83
|
+
| `project_eval` | Evaluate Ruby code in the running app context |
|
|
84
|
+
| `get_models` | List all ActiveRecord models with source locations |
|
|
85
|
+
| `get_source_location` | Find source location of any Ruby constant/method |
|
|
86
|
+
| `get_docs` | Extract documentation comments from source |
|
|
87
|
+
|
|
88
|
+
### FOSM Tools (when fosm-rails is detected)
|
|
89
|
+
|
|
90
|
+
| Tool | Description |
|
|
91
|
+
|------|-------------|
|
|
92
|
+
| `fosm_list_lifecycles` | List all FOSM lifecycle models with states/events |
|
|
93
|
+
| `fosm_inspect_lifecycle` | Deep introspection of one lifecycle |
|
|
94
|
+
| `fosm_fire_event` | Fire a lifecycle event (actor: :agent) |
|
|
95
|
+
| `fosm_transition_history` | Audit trail for a FOSM record |
|
|
96
|
+
| `fosm_available_events` | Events that can fire from current state |
|
|
97
|
+
| `fosm_why_blocked` | Explain why an event can't fire |
|
|
98
|
+
|
|
99
|
+
## ACP Agent
|
|
100
|
+
|
|
101
|
+
The ACP agent runs as a subprocess over stdio:
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
fosm-coding-agent
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Or point your ACP client at the binary:
|
|
108
|
+
|
|
109
|
+
```json
|
|
110
|
+
{
|
|
111
|
+
"agent": {
|
|
112
|
+
"command": "bundle",
|
|
113
|
+
"args": ["exec", "fosm-coding-agent"]
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Philosophy
|
|
119
|
+
|
|
120
|
+
Coding agents need **runtime intelligence**, not just static code analysis. A state machine definition in source code tells you the *structure*; runtime introspection tells you the *situation*.
|
|
121
|
+
|
|
122
|
+
FOSM (Finite Object State Machine) is a paradigm where business entities have explicit, enforced lifecycles — with guards that explain why they block, transitions that log who did what, and access controls that make autonomous agent actions safe and auditable. This gem is the bridge between FOSM's runtime observability and the coding agents that need it.
|
|
123
|
+
|
|
124
|
+
Read the FOSM paper: [Implementing Human+AI Collaboration Using Finite Object State Machine](https://www.parolkar.com/fosm)
|
|
125
|
+
|
|
126
|
+
## Development
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
bundle install
|
|
130
|
+
bundle exec rake spec
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## License
|
|
134
|
+
|
|
135
|
+
FSL-1.1-Apache-2.0 (Functional Source License, Version 1.1, Apache 2.0 Future License). See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# FOSM Rails Coding Agent — ACP agent over stdio.
|
|
5
|
+
# Spawned by ACP clients (Claude Code, Codex, etc.) to provide
|
|
6
|
+
# FOSM-aware runtime intelligence for Rails applications.
|
|
7
|
+
#
|
|
8
|
+
# Usage: fosm-coding-agent
|
|
9
|
+
|
|
10
|
+
require "bundler/setup" if File.exist?(File.expand_path("../../Gemfile", __FILE__))
|
|
11
|
+
require "fosm_rails_coding_agent"
|
|
12
|
+
require "fosm_rails_coding_agent/acp_agent"
|
|
13
|
+
|
|
14
|
+
AgentClientProtocol.run_agent(FosmRailsCodingAgent::AcpAgent.new)
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "agent_client_protocol"
|
|
4
|
+
require "json"
|
|
5
|
+
require "securerandom"
|
|
6
|
+
|
|
7
|
+
module FosmRailsCodingAgent
|
|
8
|
+
# ACP agent that implements the Agent Client Protocol.
|
|
9
|
+
# Provides push-based FOSM lifecycle context to coding agents.
|
|
10
|
+
#
|
|
11
|
+
# When a session starts, the agent proactively pushes:
|
|
12
|
+
# - All FOSM lifecycle definitions in the project
|
|
13
|
+
# - Available events for key records
|
|
14
|
+
# - Recent transition activity
|
|
15
|
+
#
|
|
16
|
+
# This is the José Valim insight: don't wait for the agent to ask,
|
|
17
|
+
# PUSH context so the coding agent starts with full situational awareness.
|
|
18
|
+
class AcpAgent
|
|
19
|
+
include AgentClientProtocol::AgentInterface
|
|
20
|
+
include AgentClientProtocol::Helpers
|
|
21
|
+
|
|
22
|
+
def on_connect(conn)
|
|
23
|
+
@conn = conn
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def initialize_agent(protocol_version:, **)
|
|
27
|
+
AgentClientProtocol::Schema::InitializeResponse.new(
|
|
28
|
+
protocol_version: AgentClientProtocol::PROTOCOL_VERSION,
|
|
29
|
+
agent_info: AgentClientProtocol::Schema::Implementation.new(
|
|
30
|
+
name: "fosm-rails-coding-agent",
|
|
31
|
+
version: FosmRailsCodingAgent::VERSION
|
|
32
|
+
),
|
|
33
|
+
capabilities: AgentClientProtocol::Schema::AgentCapabilities.new
|
|
34
|
+
)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def new_session(cwd:, **)
|
|
38
|
+
session_id = "fosm-agent-#{SecureRandom.uuid}"
|
|
39
|
+
|
|
40
|
+
# Proactively push FOSM context if available
|
|
41
|
+
push_fosm_context(session_id) if FosmRailsCodingAgent.fosm_available?
|
|
42
|
+
|
|
43
|
+
AgentClientProtocol::Schema::NewSessionResponse.new(
|
|
44
|
+
session_id: session_id
|
|
45
|
+
)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def prompt(prompt:, session_id:, **)
|
|
49
|
+
prompt.each do |block|
|
|
50
|
+
text = case block
|
|
51
|
+
when AgentClientProtocol::Schema::TextContent then block.text
|
|
52
|
+
when Hash then block["text"] || block.inspect
|
|
53
|
+
else block.inspect
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
response = handle_prompt_text(text, session_id)
|
|
57
|
+
|
|
58
|
+
@conn.session_update(
|
|
59
|
+
session_id: session_id,
|
|
60
|
+
update: update_agent_message_text(response)
|
|
61
|
+
)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
AgentClientProtocol::Schema::PromptResponse.new(
|
|
65
|
+
stop_reason: AgentClientProtocol::Schema::StopReason::END_TURN
|
|
66
|
+
)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def authenticate(method_id:, **)
|
|
70
|
+
AgentClientProtocol::Schema::AuthenticateResponse.new
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
# Push FOSM lifecycle context at session start so the coding agent
|
|
76
|
+
# has full awareness of the state machines in the project.
|
|
77
|
+
def push_fosm_context(session_id)
|
|
78
|
+
context = build_fosm_context
|
|
79
|
+
return if context.empty?
|
|
80
|
+
|
|
81
|
+
@conn.session_update(
|
|
82
|
+
session_id: session_id,
|
|
83
|
+
update: update_agent_message_text(
|
|
84
|
+
"## FOSM Lifecycle Context\n\n" \
|
|
85
|
+
"This Rails application uses FOSM (Finite Observable State Machine) " \
|
|
86
|
+
"lifecycles. Here are the registered state machines:\n\n#{context}"
|
|
87
|
+
)
|
|
88
|
+
)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def build_fosm_context
|
|
92
|
+
return "" unless defined?(::Fosm::Registry)
|
|
93
|
+
|
|
94
|
+
models = ::Fosm::Registry.model_classes
|
|
95
|
+
return "" if models.empty?
|
|
96
|
+
|
|
97
|
+
models.filter_map do |klass|
|
|
98
|
+
lifecycle = klass.fosm_lifecycle
|
|
99
|
+
next unless lifecycle
|
|
100
|
+
|
|
101
|
+
states = lifecycle.states.map do |s|
|
|
102
|
+
label = s.name.to_s
|
|
103
|
+
label += " (initial)" if s.initial?
|
|
104
|
+
label += " (terminal)" if s.terminal?
|
|
105
|
+
label
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
events = lifecycle.events.map do |e|
|
|
109
|
+
" #{e.name}: #{e.from_states.join(", ")} → #{e.to_state}"
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
"### #{klass.name}\n" \
|
|
113
|
+
"States: #{states.join(", ")}\n" \
|
|
114
|
+
"Events:\n#{events.join("\n")}\n"
|
|
115
|
+
end.join("\n")
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def handle_prompt_text(text, session_id)
|
|
119
|
+
case text.downcase.strip
|
|
120
|
+
when /lifecycles?/
|
|
121
|
+
build_fosm_context.presence || "No FOSM lifecycles found in this project."
|
|
122
|
+
when /help/
|
|
123
|
+
"FosmRailsCodingAgent ACP agent provides FOSM-aware runtime intelligence.\n" \
|
|
124
|
+
"FOSM tools are available via the MCP server at /fosm-agent/mcp.\n" \
|
|
125
|
+
"Ask about lifecycles, states, events, or transitions."
|
|
126
|
+
else
|
|
127
|
+
"FosmRailsCodingAgent is listening. Use the MCP tools at /fosm-agent/mcp for " \
|
|
128
|
+
"SQL queries, logs, code evaluation, and FOSM introspection."
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FosmRailsCodingAgent
|
|
4
|
+
# Central configuration for FosmRailsCodingAgent's MCP server and ACP agent.
|
|
5
|
+
# Set via config.fosm_coding_agent in your Rails application.
|
|
6
|
+
class Configuration
|
|
7
|
+
attr_accessor :logger,
|
|
8
|
+
:allow_remote_access,
|
|
9
|
+
:sql_row_limit,
|
|
10
|
+
:eval_timeout_ms,
|
|
11
|
+
:log_tail_default,
|
|
12
|
+
:acp_enabled,
|
|
13
|
+
:acp_agent_name,
|
|
14
|
+
:acp_push_fosm_context
|
|
15
|
+
|
|
16
|
+
def initialize
|
|
17
|
+
@logger = nil
|
|
18
|
+
@allow_remote_access = false
|
|
19
|
+
@sql_row_limit = 50
|
|
20
|
+
@eval_timeout_ms = 30_000
|
|
21
|
+
@log_tail_default = 100
|
|
22
|
+
@acp_enabled = true
|
|
23
|
+
@acp_agent_name = "fosm-rails-coding-agent"
|
|
24
|
+
@acp_push_fosm_context = true
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FosmRailsCodingAgent
|
|
4
|
+
# Intercepts Rails exception pages and injects structured error context
|
|
5
|
+
# as a hidden textarea — coding agents can parse this to understand
|
|
6
|
+
# stacktraces, controller context, and request metadata.
|
|
7
|
+
class ExceptionsMiddleware
|
|
8
|
+
def initialize(app)
|
|
9
|
+
@app = app
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def call(env)
|
|
13
|
+
request = ActionDispatch::Request.new(env)
|
|
14
|
+
status, headers, body = @app.call(env)
|
|
15
|
+
|
|
16
|
+
if (exception = request.get_header("fosm_agent.exception"))
|
|
17
|
+
formatted = format_exception(exception, request)
|
|
18
|
+
body, headers = inject_into_body(body, headers, formatted)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
[status, headers, body]
|
|
22
|
+
rescue => error
|
|
23
|
+
Rails.logger.error("[FosmRailsCodingAgent] ExceptionsMiddleware failure: #{error.message}")
|
|
24
|
+
raise
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def format_exception(exception, request)
|
|
30
|
+
backtrace = Rails.backtrace_cleaner.clean(exception.backtrace)
|
|
31
|
+
params = safe_params(request)
|
|
32
|
+
controller = params["controller"]&.camelize
|
|
33
|
+
action = params["action"]
|
|
34
|
+
|
|
35
|
+
text = exception.class.name.dup
|
|
36
|
+
text << " in #{controller}Controller" if controller
|
|
37
|
+
text << "##{action}" if action
|
|
38
|
+
text << "\n\n## Message\n\n#{exception.message}"
|
|
39
|
+
text << "\n\n## Backtrace\n\n#{backtrace.join("\n")}" if backtrace.any?
|
|
40
|
+
text << "\n\n## Request\n\n * URI: #{request.base_url}#{request.path}"
|
|
41
|
+
text << "\n * Query: #{request.query_string}"
|
|
42
|
+
|
|
43
|
+
%(<textarea style="display:none;" data-fosm-agent-exception>#{ERB::Util.html_escape(text)}</textarea>)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def safe_params(request)
|
|
47
|
+
request.parameters
|
|
48
|
+
rescue ActionController::BadRequest
|
|
49
|
+
{}
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def inject_into_body(body, headers, content)
|
|
53
|
+
html = "".dup
|
|
54
|
+
body.each { |part| html << part }
|
|
55
|
+
body.close if body.respond_to?(:close)
|
|
56
|
+
|
|
57
|
+
if (pos = html.rindex("</body>"))
|
|
58
|
+
html.insert(pos, content)
|
|
59
|
+
else
|
|
60
|
+
html << content
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
headers[Rack::CONTENT_LENGTH] = html.bytesize.to_s if headers[Rack::CONTENT_LENGTH]
|
|
64
|
+
[[html], headers]
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|