legionio 1.4.82 → 1.4.85
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 +4 -4
- data/.github/workflows/ci-cd.yml +0 -27
- data/.github/workflows/ci.yml +25 -0
- data/CHANGELOG.md +28 -0
- data/Dockerfile +3 -2
- data/lib/legion/api/governance.rb +80 -0
- data/lib/legion/api/org_chart.rb +41 -0
- data/lib/legion/api/workflow.rb +46 -0
- data/lib/legion/api.rb +8 -0
- data/lib/legion/cli/acp_command.rb +46 -0
- data/lib/legion/cli/chat_command.rb +85 -0
- data/lib/legion/cli/dashboard/renderer.rb +20 -0
- data/lib/legion/cli/init/config_generator.rb +22 -0
- data/lib/legion/cli/lex_cli_manifest.rb +67 -0
- data/lib/legion/cli/lex_command.rb +101 -0
- data/lib/legion/cli.rb +4 -0
- data/lib/legion/extensions.rb +66 -0
- data/lib/legion/helpers/context.rb +62 -0
- data/lib/legion/version.rb +1 -1
- data/public/governance/index.html +284 -0
- data/public/workflow/index.html +216 -0
- metadata +9 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 512139af5d950a28790627a762f8dc5f52e37cd093ed2829a9a42aa833ededec
|
|
4
|
+
data.tar.gz: f80a9bfa077caa791bf68058c91eb1061b9cf3851c1766ef5aedd49e9ecda95e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ecd74de131841cb6a00d1fe66ccfa51fe0a0eafe649e0cdf26e90c8602127f0d29d7f55b4cfeda0f13908b0eebe9828d61dc380a7518268e29bda49bd780855d
|
|
7
|
+
data.tar.gz: 9d27ac10782d1e6319e96b517c6da5a819e7b599f15a2e9ae27a5d1f3e33f13d70838bd64390218931ded9be7ec45c3cc28e1e65dc2cedebcbd5faf17fa1ba6a
|
data/.github/workflows/ci-cd.yml
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
name: CI/CD
|
|
2
2
|
on:
|
|
3
|
-
push:
|
|
4
|
-
branches: [main]
|
|
5
3
|
pull_request:
|
|
6
4
|
branches: [main]
|
|
7
5
|
|
|
@@ -33,31 +31,6 @@ jobs:
|
|
|
33
31
|
- run: bundle install && bundle exec rspec
|
|
34
32
|
- run: bundle exec rubocop
|
|
35
33
|
|
|
36
|
-
build:
|
|
37
|
-
name: Build Image
|
|
38
|
-
needs: test
|
|
39
|
-
if: github.ref == 'refs/heads/main'
|
|
40
|
-
runs-on: ubuntu-latest
|
|
41
|
-
permissions:
|
|
42
|
-
packages: write
|
|
43
|
-
steps:
|
|
44
|
-
- uses: actions/checkout@v4
|
|
45
|
-
- uses: docker/setup-buildx-action@v3
|
|
46
|
-
- uses: docker/login-action@v3
|
|
47
|
-
with:
|
|
48
|
-
registry: ghcr.io
|
|
49
|
-
username: ${{ github.actor }}
|
|
50
|
-
password: ${{ secrets.GITHUB_TOKEN }}
|
|
51
|
-
- uses: docker/build-push-action@v5
|
|
52
|
-
with:
|
|
53
|
-
context: .
|
|
54
|
-
push: true
|
|
55
|
-
tags: |
|
|
56
|
-
ghcr.io/legionio/legion:${{ github.sha }}
|
|
57
|
-
ghcr.io/legionio/legion:latest
|
|
58
|
-
cache-from: type=gha
|
|
59
|
-
cache-to: type=gha,mode=max
|
|
60
|
-
|
|
61
34
|
helm-lint:
|
|
62
35
|
name: Helm Lint
|
|
63
36
|
runs-on: ubuntu-latest
|
data/.github/workflows/ci.yml
CHANGED
|
@@ -27,3 +27,28 @@ jobs:
|
|
|
27
27
|
gh api repos/LegionIO/homebrew-tap/dispatches \
|
|
28
28
|
-f event_type=build-daemon \
|
|
29
29
|
-f "client_payload[legionio_version]=${{ needs.release.outputs.version }}"
|
|
30
|
+
|
|
31
|
+
docker-build:
|
|
32
|
+
name: Build Docker Image
|
|
33
|
+
needs: release
|
|
34
|
+
if: needs.release.outputs.changed == 'true'
|
|
35
|
+
runs-on: ubuntu-latest
|
|
36
|
+
permissions:
|
|
37
|
+
packages: write
|
|
38
|
+
steps:
|
|
39
|
+
- uses: actions/checkout@v4
|
|
40
|
+
- uses: docker/setup-buildx-action@v3
|
|
41
|
+
- uses: docker/login-action@v3
|
|
42
|
+
with:
|
|
43
|
+
registry: ghcr.io
|
|
44
|
+
username: ${{ github.actor }}
|
|
45
|
+
password: ${{ secrets.GITHUB_TOKEN }}
|
|
46
|
+
- uses: docker/build-push-action@v5
|
|
47
|
+
with:
|
|
48
|
+
context: .
|
|
49
|
+
push: true
|
|
50
|
+
tags: |
|
|
51
|
+
ghcr.io/legionio/legion:${{ needs.release.outputs.version }}
|
|
52
|
+
ghcr.io/legionio/legion:latest
|
|
53
|
+
cache-from: type=gha
|
|
54
|
+
cache-to: type=gha,mode=max
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# Legion Changelog
|
|
2
2
|
|
|
3
|
+
## [1.4.85] - 2026-03-20
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- `legion lex fixes` CLI command to list pending auto-fix patches (filterable by status)
|
|
7
|
+
- `legion lex approve-fix FIX_ID` CLI command to approve LLM-generated fixes
|
|
8
|
+
- `legion lex reject-fix FIX_ID` CLI command to reject LLM-generated fixes
|
|
9
|
+
- `with_data` helper to `legion lex` subcommand class for data-required operations
|
|
10
|
+
|
|
11
|
+
## [1.4.84] - 2026-03-20
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
- `Legion::Extensions.load_yaml_agents` — loads YAML/JSON agent definitions from `~/.legionio/agents/` or configured directory
|
|
15
|
+
- `generate_yaml_runner` — dynamically generates a runner Module for each agent with `llm`, `script`, and `http` function types
|
|
16
|
+
- YAML agent loading integrated into `hook_extensions` boot sequence
|
|
17
|
+
- Governance API routes under `/api/governance/approvals` (list, show, submit, approve, reject)
|
|
18
|
+
- HTML governance dashboard at `/governance/` with approve/reject buttons, 30s auto-poll, and reviewer dialog
|
|
19
|
+
- Static file serving enabled for `public/` directory in Sinatra
|
|
20
|
+
|
|
21
|
+
## [1.4.83] - 2026-03-20
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
- `Helpers::Context` for filesystem-based inter-agent context sharing
|
|
25
|
+
- Org chart API endpoint (`GET /api/org-chart`) with dashboard panel
|
|
26
|
+
- Workflow relationship graph API (`GET /api/relationships/graph`)
|
|
27
|
+
- Workflow visualizer web page (`public/workflow/`) with Cytoscape.js
|
|
28
|
+
- `--worktree` flag for `legion chat` with auto-checkpointing
|
|
29
|
+
- `.legion-context/` and `.legion-worktrees/` in generated `.gitignore`
|
|
30
|
+
|
|
3
31
|
## [1.4.82] - 2026-03-20
|
|
4
32
|
|
|
5
33
|
### Added
|
data/Dockerfile
CHANGED
|
@@ -4,8 +4,9 @@ WORKDIR /app
|
|
|
4
4
|
RUN apt-get update && \
|
|
5
5
|
apt-get install -y --no-install-recommends build-essential libpq-dev git && \
|
|
6
6
|
rm -rf /var/lib/apt/lists/*
|
|
7
|
-
COPY Gemfile
|
|
8
|
-
|
|
7
|
+
COPY Gemfile legionio.gemspec ./
|
|
8
|
+
COPY lib/legion/version.rb lib/legion/
|
|
9
|
+
RUN bundle lock && \
|
|
9
10
|
bundle config set --local without 'development test' && \
|
|
10
11
|
bundle install --jobs 4 --retry 3
|
|
11
12
|
COPY . .
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
class API < Sinatra::Base
|
|
5
|
+
module Routes
|
|
6
|
+
module Governance
|
|
7
|
+
def self.registered(app)
|
|
8
|
+
app.helpers GovernanceHelpers
|
|
9
|
+
register_approvals(app)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
module GovernanceHelpers
|
|
13
|
+
def run_governance_runner(method, **)
|
|
14
|
+
require 'legion/extensions/audit/runners/approval_queue'
|
|
15
|
+
runner = Object.new.extend(Legion::Extensions::Audit::Runners::ApprovalQueue)
|
|
16
|
+
runner.send(method, **)
|
|
17
|
+
rescue LoadError
|
|
18
|
+
halt 503, json_error('service_unavailable', 'lex-audit not available', status_code: 503)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.register_approvals(app)
|
|
23
|
+
app.get '/api/governance/approvals' do
|
|
24
|
+
require_data!
|
|
25
|
+
result = run_governance_runner(:list_pending,
|
|
26
|
+
tenant_id: params[:tenant_id],
|
|
27
|
+
limit: (params[:limit] || 50).to_i)
|
|
28
|
+
json_response(result)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
app.get '/api/governance/approvals/:id' do
|
|
32
|
+
require_data!
|
|
33
|
+
result = run_governance_runner(:show_approval, id: params[:id].to_i)
|
|
34
|
+
if result[:success]
|
|
35
|
+
json_response(result)
|
|
36
|
+
else
|
|
37
|
+
halt 404, json_error('not_found', 'Approval not found', status_code: 404)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
app.post '/api/governance/approvals' do
|
|
42
|
+
require_data!
|
|
43
|
+
body = parse_request_body
|
|
44
|
+
halt 422, json_error('missing_field', 'approval_type is required', status_code: 422) unless body[:approval_type]
|
|
45
|
+
halt 422, json_error('missing_field', 'requester_id is required', status_code: 422) unless body[:requester_id]
|
|
46
|
+
|
|
47
|
+
result = run_governance_runner(:submit,
|
|
48
|
+
approval_type: body[:approval_type],
|
|
49
|
+
payload: body[:payload] || {},
|
|
50
|
+
requester_id: body[:requester_id],
|
|
51
|
+
tenant_id: body[:tenant_id])
|
|
52
|
+
json_response(result, status_code: 201)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
app.put '/api/governance/approvals/:id/approve' do
|
|
56
|
+
require_data!
|
|
57
|
+
body = parse_request_body
|
|
58
|
+
halt 422, json_error('missing_field', 'reviewer_id is required', status_code: 422) unless body[:reviewer_id]
|
|
59
|
+
|
|
60
|
+
result = run_governance_runner(:approve,
|
|
61
|
+
id: params[:id].to_i,
|
|
62
|
+
reviewer_id: body[:reviewer_id])
|
|
63
|
+
json_response(result)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
app.put '/api/governance/approvals/:id/reject' do
|
|
67
|
+
require_data!
|
|
68
|
+
body = parse_request_body
|
|
69
|
+
halt 422, json_error('missing_field', 'reviewer_id is required', status_code: 422) unless body[:reviewer_id]
|
|
70
|
+
|
|
71
|
+
result = run_governance_runner(:reject,
|
|
72
|
+
id: params[:id].to_i,
|
|
73
|
+
reviewer_id: body[:reviewer_id])
|
|
74
|
+
json_response(result)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
class API < Sinatra::Base
|
|
5
|
+
module Routes
|
|
6
|
+
module OrgChart
|
|
7
|
+
def self.registered(app)
|
|
8
|
+
app.helpers OrgChartHelpers
|
|
9
|
+
app.get '/api/org-chart' do
|
|
10
|
+
require_data!
|
|
11
|
+
departments = build_org_chart
|
|
12
|
+
json_response({ departments: departments })
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
module OrgChartHelpers
|
|
17
|
+
def build_org_chart
|
|
18
|
+
extensions = Legion::Data::Model::Extension.all
|
|
19
|
+
workers = Legion::Data::Model::DigitalWorker.all
|
|
20
|
+
|
|
21
|
+
extensions.map do |ext|
|
|
22
|
+
functions = Legion::Data::Model::Function.where(extension_id: ext.id).all
|
|
23
|
+
{
|
|
24
|
+
name: ext.name,
|
|
25
|
+
roles: functions.map do |func|
|
|
26
|
+
ext_workers = workers.select { |w| w.extension_name == ext.name }
|
|
27
|
+
{
|
|
28
|
+
name: func.name,
|
|
29
|
+
workers: ext_workers.map { |w| { id: w.id, name: w.name, status: w.lifecycle_state } }
|
|
30
|
+
}
|
|
31
|
+
end
|
|
32
|
+
}
|
|
33
|
+
end
|
|
34
|
+
rescue StandardError
|
|
35
|
+
[]
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
class API < Sinatra::Base
|
|
5
|
+
module Routes
|
|
6
|
+
module Workflow
|
|
7
|
+
def self.registered(app)
|
|
8
|
+
app.helpers WorkflowHelpers
|
|
9
|
+
app.get '/api/relationships/graph' do
|
|
10
|
+
require_data!
|
|
11
|
+
graph = build_relationship_graph(
|
|
12
|
+
chain_id: params[:chain_id]&.to_i,
|
|
13
|
+
extension: params[:extension]
|
|
14
|
+
)
|
|
15
|
+
json_response(graph)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
module WorkflowHelpers
|
|
20
|
+
def build_relationship_graph(chain_id: nil, extension: nil)
|
|
21
|
+
raw = Legion::Graph::Builder.build(chain_id: chain_id)
|
|
22
|
+
|
|
23
|
+
nodes = raw[:nodes].map do |id, meta|
|
|
24
|
+
ext = id.to_s.split('.').first
|
|
25
|
+
{ id: id, label: meta[:label], type: meta[:type], extension: ext }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
edges = raw[:edges].map do |edge|
|
|
29
|
+
{ source: edge[:from], target: edge[:to], label: edge[:label] }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
if extension
|
|
33
|
+
node_ids = nodes.select { |n| n[:extension] == extension }.map { |n| n[:id] }
|
|
34
|
+
nodes = nodes.select { |n| node_ids.include?(n[:id]) }
|
|
35
|
+
edges = edges.select { |e| node_ids.include?(e[:source]) || node_ids.include?(e[:target]) }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
{ nodes: nodes, edges: edges }
|
|
39
|
+
rescue StandardError
|
|
40
|
+
{ nodes: [], edges: [] }
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
data/lib/legion/api.rb
CHANGED
|
@@ -34,6 +34,9 @@ require_relative 'api/audit'
|
|
|
34
34
|
require_relative 'api/metrics'
|
|
35
35
|
require_relative 'api/llm'
|
|
36
36
|
require_relative 'api/catalog'
|
|
37
|
+
require_relative 'api/org_chart'
|
|
38
|
+
require_relative 'api/workflow'
|
|
39
|
+
require_relative 'api/governance'
|
|
37
40
|
|
|
38
41
|
module Legion
|
|
39
42
|
class API < Sinatra::Base
|
|
@@ -42,6 +45,8 @@ module Legion
|
|
|
42
45
|
|
|
43
46
|
set :show_exceptions, false
|
|
44
47
|
set :raise_errors, false
|
|
48
|
+
set :public_folder, File.expand_path('../../public', __dir__)
|
|
49
|
+
set :static, true
|
|
45
50
|
|
|
46
51
|
configure do
|
|
47
52
|
set :logging, nil
|
|
@@ -91,6 +96,7 @@ module Legion
|
|
|
91
96
|
register Routes::Extensions
|
|
92
97
|
register Routes::Nodes
|
|
93
98
|
register Routes::Schedules
|
|
99
|
+
register Routes::Workflow
|
|
94
100
|
register Routes::Relationships
|
|
95
101
|
register Routes::Chains
|
|
96
102
|
register Routes::Settings
|
|
@@ -110,6 +116,8 @@ module Legion
|
|
|
110
116
|
register Routes::Metrics
|
|
111
117
|
register Routes::Llm
|
|
112
118
|
register Routes::ExtensionCatalog
|
|
119
|
+
register Routes::OrgChart
|
|
120
|
+
register Routes::Governance
|
|
113
121
|
|
|
114
122
|
use Legion::Rbac::Middleware if defined?(Legion::Rbac::Middleware)
|
|
115
123
|
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module CLI
|
|
5
|
+
class Acp < Thor
|
|
6
|
+
def self.exit_on_failure?
|
|
7
|
+
true
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
desc 'stdio', 'Start ACP agent with stdio transport (default)'
|
|
11
|
+
def stdio
|
|
12
|
+
require 'legion/extensions/acp'
|
|
13
|
+
|
|
14
|
+
transport = Legion::Extensions::Acp::Transport::Stdio.new
|
|
15
|
+
agent = Legion::Extensions::Acp::Runners::Agent.new(transport: transport)
|
|
16
|
+
|
|
17
|
+
transport.log('LegionIO ACP agent started (stdio)')
|
|
18
|
+
|
|
19
|
+
setup_llm if llm_available?
|
|
20
|
+
|
|
21
|
+
transport.run { |msg| agent.dispatch(msg) }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
default_command :stdio
|
|
25
|
+
|
|
26
|
+
no_commands do
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def llm_available?
|
|
30
|
+
require 'legion/llm'
|
|
31
|
+
true
|
|
32
|
+
rescue LoadError
|
|
33
|
+
false
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def setup_llm
|
|
37
|
+
require 'legion/cli/connection'
|
|
38
|
+
Connection.ensure_settings
|
|
39
|
+
Connection.ensure_llm
|
|
40
|
+
rescue StandardError => e
|
|
41
|
+
warn("[lex-acp] LLM setup failed: #{e.message} — running without prompt support")
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -30,6 +30,7 @@ module Legion
|
|
|
30
30
|
class_option :fork, type: :string, desc: 'Fork a saved session (load but save as new)'
|
|
31
31
|
class_option :add_dir, type: :array, default: [], desc: 'Additional directories to include in context'
|
|
32
32
|
class_option :personality, type: :string, desc: 'Communication style (concise, verbose, educational)'
|
|
33
|
+
class_option :worktree, type: :boolean, default: false, desc: 'Run in isolated git worktree'
|
|
33
34
|
|
|
34
35
|
autoload :Session, 'legion/cli/chat/session'
|
|
35
36
|
autoload :StatusIndicator, 'legion/cli/chat/status_indicator'
|
|
@@ -54,6 +55,7 @@ module Legion
|
|
|
54
55
|
load_custom_agents
|
|
55
56
|
|
|
56
57
|
setup_notification_bridge
|
|
58
|
+
setup_worktree(out) if options[:worktree]
|
|
57
59
|
|
|
58
60
|
chat_log.info "session started model=#{@session.model_id} incognito=#{incognito?}"
|
|
59
61
|
out.banner(version: Legion::VERSION)
|
|
@@ -311,6 +313,7 @@ module Legion
|
|
|
311
313
|
index: tool_index, total: tool_total
|
|
312
314
|
})
|
|
313
315
|
puts out.dim(" [result] #{result_preview}")
|
|
316
|
+
worktree_auto_checkpoint
|
|
314
317
|
}
|
|
315
318
|
) do |chunk|
|
|
316
319
|
buffer << chunk.content if chunk.content
|
|
@@ -409,6 +412,7 @@ module Legion
|
|
|
409
412
|
when '/quit', '/exit', '/q'
|
|
410
413
|
show_session_stats(out)
|
|
411
414
|
auto_save_session(out)
|
|
415
|
+
cleanup_worktree(out) if @worktree_path
|
|
412
416
|
raise SystemExit, 0
|
|
413
417
|
when '/help', '/h'
|
|
414
418
|
show_help(out)
|
|
@@ -774,6 +778,11 @@ module Legion
|
|
|
774
778
|
end
|
|
775
779
|
|
|
776
780
|
def handle_rewind(arg, out)
|
|
781
|
+
if @worktree_path
|
|
782
|
+
handle_worktree_rewind(arg, out)
|
|
783
|
+
return
|
|
784
|
+
end
|
|
785
|
+
|
|
777
786
|
require 'legion/cli/chat/checkpoint'
|
|
778
787
|
if Chat::Checkpoint.entries.none?
|
|
779
788
|
out.warn('No checkpoints available to rewind.')
|
|
@@ -1149,6 +1158,82 @@ module Legion
|
|
|
1149
1158
|
def api_port_for_chat
|
|
1150
1159
|
4567
|
|
1151
1160
|
end
|
|
1161
|
+
|
|
1162
|
+
def setup_worktree(out)
|
|
1163
|
+
require 'legion/extensions/exec/helpers/worktree'
|
|
1164
|
+
@worktree_task_id = "chat-#{SecureRandom.hex(4)}"
|
|
1165
|
+
@checkpoint_count = 0
|
|
1166
|
+
wt = Legion::Extensions::Exec::Helpers::Worktree.create(task_id: @worktree_task_id)
|
|
1167
|
+
if wt[:success]
|
|
1168
|
+
@worktree_path = wt[:path]
|
|
1169
|
+
Dir.chdir(@worktree_path)
|
|
1170
|
+
out.success("Worktree created: #{@worktree_path} (branch: #{wt[:branch]})")
|
|
1171
|
+
chat_log.info "worktree created path=#{@worktree_path} branch=#{wt[:branch]}"
|
|
1172
|
+
else
|
|
1173
|
+
out.warn("Worktree creation failed: #{wt[:reason]}. Continuing without worktree.")
|
|
1174
|
+
chat_log.warn "worktree creation failed: #{wt.inspect}"
|
|
1175
|
+
end
|
|
1176
|
+
rescue LoadError
|
|
1177
|
+
out.warn('lex-exec not available. --worktree requires lex-exec. Continuing without worktree.')
|
|
1178
|
+
end
|
|
1179
|
+
|
|
1180
|
+
def worktree_auto_checkpoint
|
|
1181
|
+
return unless @worktree_path
|
|
1182
|
+
|
|
1183
|
+
require 'legion/extensions/exec/helpers/checkpoint'
|
|
1184
|
+
@checkpoint_count += 1
|
|
1185
|
+
Legion::Extensions::Exec::Helpers::Checkpoint.save(
|
|
1186
|
+
worktree_path: @worktree_path,
|
|
1187
|
+
label: "step-#{@checkpoint_count}",
|
|
1188
|
+
task_id: @worktree_task_id
|
|
1189
|
+
)
|
|
1190
|
+
rescue StandardError => e
|
|
1191
|
+
chat_log.debug "worktree checkpoint failed: #{e.message}"
|
|
1192
|
+
end
|
|
1193
|
+
|
|
1194
|
+
def handle_worktree_rewind(arg, out)
|
|
1195
|
+
require 'legion/extensions/exec/helpers/checkpoint'
|
|
1196
|
+
list = Legion::Extensions::Exec::Helpers::Checkpoint.list_checkpoints(task_id: @worktree_task_id)
|
|
1197
|
+
if list[:checkpoints].empty?
|
|
1198
|
+
out.warn('No worktree checkpoints available.')
|
|
1199
|
+
return
|
|
1200
|
+
end
|
|
1201
|
+
|
|
1202
|
+
label = if arg.nil? || arg.strip.empty?
|
|
1203
|
+
list[:checkpoints].last[:label]
|
|
1204
|
+
elsif arg.strip.match?(/\A\d+\z/)
|
|
1205
|
+
target = [list[:checkpoints].size - arg.strip.to_i, 0].max
|
|
1206
|
+
list[:checkpoints][target][:label]
|
|
1207
|
+
else
|
|
1208
|
+
arg.strip
|
|
1209
|
+
end
|
|
1210
|
+
|
|
1211
|
+
result = Legion::Extensions::Exec::Helpers::Checkpoint.restore(
|
|
1212
|
+
worktree_path: @worktree_path, label: label, task_id: @worktree_task_id
|
|
1213
|
+
)
|
|
1214
|
+
if result[:success]
|
|
1215
|
+
chat_log.info "worktree rewind to #{label}"
|
|
1216
|
+
out.success("Restored to checkpoint: #{label}")
|
|
1217
|
+
else
|
|
1218
|
+
out.warn("Rewind failed: #{result[:message]}")
|
|
1219
|
+
end
|
|
1220
|
+
end
|
|
1221
|
+
|
|
1222
|
+
def cleanup_worktree(out)
|
|
1223
|
+
require 'legion/extensions/exec/helpers/worktree'
|
|
1224
|
+
require 'legion/extensions/exec/helpers/checkpoint'
|
|
1225
|
+
|
|
1226
|
+
checkpoints = Legion::Extensions::Exec::Helpers::Checkpoint.list_checkpoints(task_id: @worktree_task_id)
|
|
1227
|
+
if checkpoints[:checkpoints].any?
|
|
1228
|
+
out.info("Worktree has #{checkpoints[:checkpoints].size} checkpoint(s). Branch: legion/#{@worktree_task_id}")
|
|
1229
|
+
out.info('Worktree preserved. Merge manually or run: git worktree remove <path>')
|
|
1230
|
+
else
|
|
1231
|
+
Legion::Extensions::Exec::Helpers::Worktree.remove(task_id: @worktree_task_id)
|
|
1232
|
+
out.info('Worktree removed (no checkpoints).')
|
|
1233
|
+
end
|
|
1234
|
+
rescue StandardError => e
|
|
1235
|
+
chat_log.warn "worktree cleanup error: #{e.message}"
|
|
1236
|
+
end
|
|
1152
1237
|
end
|
|
1153
1238
|
end
|
|
1154
1239
|
end
|
|
@@ -17,6 +17,8 @@ module Legion
|
|
|
17
17
|
lines << events_section(data[:events] || [])
|
|
18
18
|
lines << separator
|
|
19
19
|
lines << health_section(data[:health] || {})
|
|
20
|
+
lines << separator
|
|
21
|
+
lines << org_chart_section(data[:departments] || [])
|
|
20
22
|
lines << footer_line(data[:fetched_at])
|
|
21
23
|
lines.flatten.join("\n")
|
|
22
24
|
end
|
|
@@ -63,6 +65,24 @@ module Legion
|
|
|
63
65
|
"Health: #{components.empty? ? 'unknown' : components}"
|
|
64
66
|
end
|
|
65
67
|
|
|
68
|
+
def org_chart_section(departments)
|
|
69
|
+
lines = ['Org Chart:']
|
|
70
|
+
if departments.empty?
|
|
71
|
+
lines << ' (no departments)'
|
|
72
|
+
else
|
|
73
|
+
departments.each do |dept|
|
|
74
|
+
lines << " #{dept[:name]}"
|
|
75
|
+
(dept[:roles] || []).each do |role|
|
|
76
|
+
lines << " +-- #{role[:name]}"
|
|
77
|
+
(role[:workers] || []).each do |w|
|
|
78
|
+
lines << " | +-- #{w[:name]} (#{w[:status]})"
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
lines
|
|
84
|
+
end
|
|
85
|
+
|
|
66
86
|
def footer_line(fetched_at)
|
|
67
87
|
"Last updated: #{fetched_at&.strftime('%H:%M:%S') || 'never'} | Press q to quit, r to refresh"
|
|
68
88
|
end
|
|
@@ -39,11 +39,33 @@ module Legion
|
|
|
39
39
|
settings_path = File.join(workspace_dir, 'settings.json')
|
|
40
40
|
File.write(settings_path, "{}\n") unless File.exist?(settings_path)
|
|
41
41
|
|
|
42
|
+
ensure_gitignore_entries(dir)
|
|
43
|
+
|
|
42
44
|
workspace_dir
|
|
43
45
|
end
|
|
44
46
|
|
|
47
|
+
GITIGNORE_ENTRIES = %w[
|
|
48
|
+
.legion-context/
|
|
49
|
+
.legion-worktrees/
|
|
50
|
+
].freeze
|
|
51
|
+
|
|
45
52
|
private
|
|
46
53
|
|
|
54
|
+
def ensure_gitignore_entries(dir)
|
|
55
|
+
gitignore_path = File.join(dir, '.gitignore')
|
|
56
|
+
existing = File.exist?(gitignore_path) ? File.read(gitignore_path) : ''
|
|
57
|
+
existing_lines = existing.lines.map(&:chomp)
|
|
58
|
+
|
|
59
|
+
additions = GITIGNORE_ENTRIES.reject { |entry| existing_lines.include?(entry) }
|
|
60
|
+
return if additions.empty?
|
|
61
|
+
|
|
62
|
+
content = existing
|
|
63
|
+
content += "\n" unless content.empty? || content.end_with?("\n")
|
|
64
|
+
content += "# Legion workspace\n" unless existing_lines.any? { |l| l.include?('Legion') }
|
|
65
|
+
content += "#{additions.join("\n")}\n"
|
|
66
|
+
File.write(gitignore_path, content)
|
|
67
|
+
end
|
|
68
|
+
|
|
47
69
|
def render_template(path, options)
|
|
48
70
|
template = File.read(path)
|
|
49
71
|
ERB.new(template, trim_mode: '-').result_with_hash(options: options)
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
|
|
6
|
+
module Legion
|
|
7
|
+
module CLI
|
|
8
|
+
class LexCliManifest
|
|
9
|
+
attr_reader :cache_dir
|
|
10
|
+
|
|
11
|
+
def initialize(cache_dir: File.expand_path('~/.legionio/cache/cli'))
|
|
12
|
+
@cache_dir = cache_dir
|
|
13
|
+
FileUtils.mkdir_p(@cache_dir)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def write_manifest(gem_name:, gem_version:, alias_name:, commands:)
|
|
17
|
+
data = { 'gem' => gem_name, 'version' => gem_version, 'alias' => alias_name,
|
|
18
|
+
'commands' => serialize_commands(commands) }
|
|
19
|
+
File.write(manifest_path(gem_name), ::JSON.pretty_generate(data))
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def read_manifest(gem_name)
|
|
23
|
+
path = manifest_path(gem_name)
|
|
24
|
+
return nil unless File.exist?(path)
|
|
25
|
+
|
|
26
|
+
::JSON.parse(File.read(path))
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def resolve_alias(name)
|
|
30
|
+
all_manifests.each do |m|
|
|
31
|
+
return m['gem'] if m['alias'] == name
|
|
32
|
+
end
|
|
33
|
+
nil
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def all_manifests
|
|
37
|
+
Dir.glob(File.join(@cache_dir, 'lex-*.json')).map do |path|
|
|
38
|
+
::JSON.parse(File.read(path))
|
|
39
|
+
rescue StandardError
|
|
40
|
+
nil
|
|
41
|
+
end.compact
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def stale?(gem_name, current_version)
|
|
45
|
+
m = read_manifest(gem_name)
|
|
46
|
+
return true unless m
|
|
47
|
+
|
|
48
|
+
m['version'] != current_version
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def manifest_path(gem_name)
|
|
54
|
+
File.join(@cache_dir, "#{gem_name}.json")
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def serialize_commands(commands)
|
|
58
|
+
commands.transform_values do |cmd|
|
|
59
|
+
{
|
|
60
|
+
'class' => cmd[:class_name],
|
|
61
|
+
'methods' => cmd[:methods].transform_values { |m| { 'desc' => m[:desc], 'args' => m[:args] } }
|
|
62
|
+
}
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|