aidp 0.17.1 → 0.19.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.
- checksums.yaml +4 -4
- data/README.md +69 -0
- data/lib/aidp/cli/terminal_io.rb +5 -2
- data/lib/aidp/cli.rb +43 -2
- data/lib/aidp/config.rb +9 -14
- data/lib/aidp/execute/agent_signal_parser.rb +20 -0
- data/lib/aidp/execute/persistent_tasklist.rb +220 -0
- data/lib/aidp/execute/prompt_manager.rb +128 -1
- data/lib/aidp/execute/repl_macros.rb +719 -0
- data/lib/aidp/execute/work_loop_runner.rb +162 -1
- data/lib/aidp/harness/ai_decision_engine.rb +376 -0
- data/lib/aidp/harness/capability_registry.rb +273 -0
- data/lib/aidp/harness/config_schema.rb +305 -1
- data/lib/aidp/harness/configuration.rb +452 -0
- data/lib/aidp/harness/enhanced_runner.rb +7 -1
- data/lib/aidp/harness/provider_factory.rb +0 -2
- data/lib/aidp/harness/runner.rb +7 -1
- data/lib/aidp/harness/thinking_depth_manager.rb +335 -0
- data/lib/aidp/harness/zfc_condition_detector.rb +395 -0
- data/lib/aidp/init/devcontainer_generator.rb +274 -0
- data/lib/aidp/init/runner.rb +37 -10
- data/lib/aidp/init.rb +1 -0
- data/lib/aidp/prompt_optimization/context_composer.rb +286 -0
- data/lib/aidp/prompt_optimization/optimizer.rb +335 -0
- data/lib/aidp/prompt_optimization/prompt_builder.rb +309 -0
- data/lib/aidp/prompt_optimization/relevance_scorer.rb +256 -0
- data/lib/aidp/prompt_optimization/source_code_fragmenter.rb +308 -0
- data/lib/aidp/prompt_optimization/style_guide_indexer.rb +240 -0
- data/lib/aidp/prompt_optimization/template_indexer.rb +250 -0
- data/lib/aidp/provider_manager.rb +0 -2
- data/lib/aidp/providers/anthropic.rb +19 -0
- data/lib/aidp/setup/wizard.rb +299 -4
- data/lib/aidp/utils/devcontainer_detector.rb +166 -0
- data/lib/aidp/version.rb +1 -1
- data/lib/aidp/watch/build_processor.rb +72 -6
- data/lib/aidp/watch/repository_client.rb +2 -1
- data/lib/aidp.rb +1 -1
- data/templates/aidp.yml.example +128 -0
- metadata +15 -2
- data/lib/aidp/providers/macos_ui.rb +0 -102
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 220c03eda20360cfa8b954bc578b15fe766742e849037539f1a1f9c794310569
|
|
4
|
+
data.tar.gz: 0fa8cc7ff18ab1ee66c11f187df4554f84504d4713322f089427b9d921de5420
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 772f8bc2099fce5b1d8ff0c9d8567382281851b7d3a283eb0aa847ad92d5f420b9aff91c702424df89cf6570cc59cfe35dacc8baf7df60b0d019cd1dfc97ceea
|
|
7
|
+
data.tar.gz: 1bad64d70c87752e7416bbad5558ff9cab2e67902b2e461bbd1ad946a4659951949ea4f6c0642a2424d81a6119e405f9208647e5d7c229d13626fc9f0774bae7
|
data/README.md
CHANGED
|
@@ -42,6 +42,75 @@ You can re-run the wizard manually:
|
|
|
42
42
|
aidp --setup-config
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
+
## Devcontainer Support
|
|
46
|
+
|
|
47
|
+
AIDP provides first-class devcontainer support for sandboxed, secure AI agent execution. Devcontainers offer:
|
|
48
|
+
|
|
49
|
+
- **Network Security**: Strict firewall with allowlisted domains only
|
|
50
|
+
- **Sandboxed Environment**: Isolated from your host system
|
|
51
|
+
- **Elevated Permissions**: AI agents can run with full permissions inside the container
|
|
52
|
+
- **Consistent Setup**: Same environment across all developers
|
|
53
|
+
|
|
54
|
+
### For AIDP Development
|
|
55
|
+
|
|
56
|
+
This repository includes a `.devcontainer/` setup for developing AIDP itself:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
# Open in VS Code
|
|
60
|
+
code .
|
|
61
|
+
|
|
62
|
+
# Press F1 → "Dev Containers: Reopen in Container"
|
|
63
|
+
# Container builds automatically with Ruby 3.4.5, all tools, and firewall
|
|
64
|
+
|
|
65
|
+
# Run tests inside container
|
|
66
|
+
bundle exec rspec
|
|
67
|
+
|
|
68
|
+
# Run AIDP inside container
|
|
69
|
+
bundle exec aidp
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
See [.devcontainer/README.md](.devcontainer/README.md) for complete documentation.
|
|
73
|
+
|
|
74
|
+
### Generating Devcontainers for Your Projects
|
|
75
|
+
|
|
76
|
+
Use `aidp init` to generate a devcontainer for any project:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
# Initialize project with devcontainer
|
|
80
|
+
aidp init
|
|
81
|
+
|
|
82
|
+
# When prompted:
|
|
83
|
+
# "Generate devcontainer configuration for sandboxed development?" → Yes
|
|
84
|
+
|
|
85
|
+
# Or use the flag directly
|
|
86
|
+
aidp init --with-devcontainer
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
This creates:
|
|
90
|
+
|
|
91
|
+
- `.devcontainer/Dockerfile` - Customized for your project's language/framework
|
|
92
|
+
- `.devcontainer/devcontainer.json` - VS Code configuration and extensions
|
|
93
|
+
- `.devcontainer/init-firewall.sh` - Network security rules
|
|
94
|
+
- `.devcontainer/README.md` - Setup and usage documentation
|
|
95
|
+
|
|
96
|
+
### Elevated Permissions in Devcontainers
|
|
97
|
+
|
|
98
|
+
When running inside a devcontainer, you can enable elevated permissions for AI agents:
|
|
99
|
+
|
|
100
|
+
```yaml
|
|
101
|
+
# aidp.yml
|
|
102
|
+
devcontainer:
|
|
103
|
+
enabled: true
|
|
104
|
+
full_permissions_when_in_devcontainer: true # Run all providers with full permissions
|
|
105
|
+
|
|
106
|
+
# Or enable per-provider
|
|
107
|
+
permissions:
|
|
108
|
+
skip_permission_checks:
|
|
109
|
+
- claude # Adds --dangerously-skip-permissions for Claude Code
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
AIDP automatically detects when it's running in a devcontainer and adjusts agent permissions accordingly. This is safe because the container is sandboxed from your host system.
|
|
113
|
+
|
|
45
114
|
## Core Features
|
|
46
115
|
|
|
47
116
|
### Work Loops
|
data/lib/aidp/cli/terminal_io.rb
CHANGED
|
@@ -51,8 +51,11 @@ module Aidp
|
|
|
51
51
|
interrupt: :exit
|
|
52
52
|
)
|
|
53
53
|
|
|
54
|
-
#
|
|
55
|
-
result = reader.read_line(prompt
|
|
54
|
+
# TTY::Reader#read_line does not support a :default keyword; we emulate fallback
|
|
55
|
+
result = reader.read_line(prompt)
|
|
56
|
+
if (result.nil? || result.chomp.empty?) && !default.nil?
|
|
57
|
+
return default
|
|
58
|
+
end
|
|
56
59
|
result&.chomp
|
|
57
60
|
rescue TTY::Reader::InputInterrupt
|
|
58
61
|
raise Interrupt
|
data/lib/aidp/cli.rb
CHANGED
|
@@ -759,9 +759,10 @@ module Aidp
|
|
|
759
759
|
require_relative "harness/provider_info"
|
|
760
760
|
|
|
761
761
|
provider_name = args.shift
|
|
762
|
+
|
|
763
|
+
# If no provider specified, show models catalog table
|
|
762
764
|
unless provider_name
|
|
763
|
-
|
|
764
|
-
display_message("Example: aidp providers info claude", type: :info)
|
|
765
|
+
run_providers_models_catalog
|
|
765
766
|
return
|
|
766
767
|
end
|
|
767
768
|
|
|
@@ -839,6 +840,46 @@ module Aidp
|
|
|
839
840
|
display_message("Tip: Use --refresh to update this information", type: :muted)
|
|
840
841
|
end
|
|
841
842
|
|
|
843
|
+
def run_providers_models_catalog
|
|
844
|
+
require_relative "harness/capability_registry"
|
|
845
|
+
require "tty-table"
|
|
846
|
+
|
|
847
|
+
display_message("Models Catalog - Thinking Depth Tiers", type: :highlight)
|
|
848
|
+
display_message("=" * 80, type: :muted)
|
|
849
|
+
|
|
850
|
+
registry = Aidp::Harness::CapabilityRegistry.new
|
|
851
|
+
unless registry.load_catalog
|
|
852
|
+
display_message("No models catalog found. Create .aidp/models_catalog.yml first.", type: :error)
|
|
853
|
+
return
|
|
854
|
+
end
|
|
855
|
+
|
|
856
|
+
rows = []
|
|
857
|
+
registry.provider_names.sort.each do |provider|
|
|
858
|
+
models = registry.models_for_provider(provider)
|
|
859
|
+
models.each do |model_name, model_data|
|
|
860
|
+
tier = model_data["tier"] || "-"
|
|
861
|
+
context = model_data["context_window"] ? "#{model_data["context_window"] / 1000}k" : "-"
|
|
862
|
+
tools = model_data["supports_tools"] ? "yes" : "no"
|
|
863
|
+
cost_input = model_data["cost_per_mtok_input"]
|
|
864
|
+
cost = cost_input ? "$#{cost_input}/MTok" : "-"
|
|
865
|
+
|
|
866
|
+
rows << [provider, model_name, tier, context, tools, cost]
|
|
867
|
+
end
|
|
868
|
+
end
|
|
869
|
+
|
|
870
|
+
if rows.empty?
|
|
871
|
+
display_message("No models found in catalog", type: :info)
|
|
872
|
+
return
|
|
873
|
+
end
|
|
874
|
+
|
|
875
|
+
header = ["Provider", "Model", "Tier", "Context", "Tools", "Cost"]
|
|
876
|
+
table = TTY::Table.new(header, rows)
|
|
877
|
+
display_message(table.render(:basic), type: :info)
|
|
878
|
+
|
|
879
|
+
display_message("\n" + "=" * 80, type: :muted)
|
|
880
|
+
display_message("Use '/thinking show' in REPL to see current tier configuration", type: :muted)
|
|
881
|
+
end
|
|
882
|
+
|
|
842
883
|
def run_providers_refresh_command(args)
|
|
843
884
|
require_relative "harness/provider_info"
|
|
844
885
|
require "tty-spinner"
|
data/lib/aidp/config.rb
CHANGED
|
@@ -15,8 +15,7 @@ module Aidp
|
|
|
15
15
|
no_api_keys_required: false,
|
|
16
16
|
provider_weights: {
|
|
17
17
|
"cursor" => 3,
|
|
18
|
-
"anthropic" => 2
|
|
19
|
-
"macos" => 1
|
|
18
|
+
"anthropic" => 2
|
|
20
19
|
},
|
|
21
20
|
circuit_breaker: {
|
|
22
21
|
enabled: true,
|
|
@@ -74,6 +73,7 @@ module Aidp
|
|
|
74
73
|
cursor: {
|
|
75
74
|
type: "subscription",
|
|
76
75
|
priority: 1,
|
|
76
|
+
model_family: "auto",
|
|
77
77
|
default_flags: [],
|
|
78
78
|
models: ["cursor-default", "cursor-fast", "cursor-precise"],
|
|
79
79
|
model_weights: {
|
|
@@ -108,6 +108,7 @@ module Aidp
|
|
|
108
108
|
anthropic: {
|
|
109
109
|
type: "usage_based",
|
|
110
110
|
priority: 2,
|
|
111
|
+
model_family: "claude",
|
|
111
112
|
max_tokens: 100_000,
|
|
112
113
|
default_flags: ["--dangerously-skip-permissions"],
|
|
113
114
|
models: ["claude-3-5-sonnet-20241022", "claude-3-5-haiku-20241022", "claude-3-opus-20240229"],
|
|
@@ -149,18 +150,6 @@ module Aidp
|
|
|
149
150
|
enabled: true,
|
|
150
151
|
metrics_interval: 60
|
|
151
152
|
}
|
|
152
|
-
},
|
|
153
|
-
macos: {
|
|
154
|
-
type: "passthrough",
|
|
155
|
-
priority: 4,
|
|
156
|
-
underlying_service: "cursor",
|
|
157
|
-
models: ["cursor-chat"],
|
|
158
|
-
features: {
|
|
159
|
-
file_upload: false,
|
|
160
|
-
code_generation: true,
|
|
161
|
-
analysis: true,
|
|
162
|
-
interactive: true
|
|
163
|
-
}
|
|
164
153
|
}
|
|
165
154
|
},
|
|
166
155
|
skills: {
|
|
@@ -342,6 +331,12 @@ module Aidp
|
|
|
342
331
|
merged[:skills] = merged[:skills].merge(symbolize_keys(skills_section))
|
|
343
332
|
end
|
|
344
333
|
|
|
334
|
+
# Deep merge thinking config
|
|
335
|
+
if config[:thinking] || config["thinking"]
|
|
336
|
+
thinking_section = config[:thinking] || config["thinking"]
|
|
337
|
+
merged[:thinking] = symbolize_keys(thinking_section)
|
|
338
|
+
end
|
|
339
|
+
|
|
345
340
|
merged
|
|
346
341
|
end
|
|
347
342
|
|
|
@@ -16,6 +16,26 @@ module Aidp
|
|
|
16
16
|
nil
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
+
# Parse task filing signals from agent output
|
|
20
|
+
# Returns array of task hashes with description, priority, and tags
|
|
21
|
+
def self.parse_task_filing(output)
|
|
22
|
+
return [] unless output
|
|
23
|
+
|
|
24
|
+
tasks = []
|
|
25
|
+
# Pattern: File task: "description" [priority: high|medium|low] [tags: tag1,tag2]
|
|
26
|
+
pattern = /File\s+task:\s*"([^"]+)"(?:\s+priority:\s*(high|medium|low))?(?:\s+tags:\s*([^\s]+))?/i
|
|
27
|
+
|
|
28
|
+
output.to_s.scan(pattern).each do |description, priority, tags|
|
|
29
|
+
tasks << {
|
|
30
|
+
description: description.strip,
|
|
31
|
+
priority: (priority || "medium").downcase.to_sym,
|
|
32
|
+
tags: tags ? tags.split(",").map(&:strip) : []
|
|
33
|
+
}
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
tasks
|
|
37
|
+
end
|
|
38
|
+
|
|
19
39
|
def self.normalize_token(raw)
|
|
20
40
|
return nil if raw.nil? || raw.empty?
|
|
21
41
|
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "time"
|
|
5
|
+
require "securerandom"
|
|
6
|
+
require "fileutils"
|
|
7
|
+
|
|
8
|
+
module Aidp
|
|
9
|
+
module Execute
|
|
10
|
+
# Task struct for persistent tasklist entries
|
|
11
|
+
Task = Struct.new(
|
|
12
|
+
:id,
|
|
13
|
+
:description,
|
|
14
|
+
:status,
|
|
15
|
+
:priority,
|
|
16
|
+
:created_at,
|
|
17
|
+
:updated_at,
|
|
18
|
+
:session,
|
|
19
|
+
:discovered_during,
|
|
20
|
+
:started_at,
|
|
21
|
+
:completed_at,
|
|
22
|
+
:abandoned_at,
|
|
23
|
+
:abandoned_reason,
|
|
24
|
+
:tags,
|
|
25
|
+
keyword_init: true
|
|
26
|
+
) do
|
|
27
|
+
def to_h
|
|
28
|
+
super.compact
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Persistent tasklist for tracking tasks across sessions
|
|
33
|
+
# Uses append-only JSONL format for git-friendly storage
|
|
34
|
+
class PersistentTasklist
|
|
35
|
+
attr_reader :project_dir, :file_path
|
|
36
|
+
|
|
37
|
+
class TaskNotFoundError < StandardError; end
|
|
38
|
+
class InvalidTaskError < StandardError; end
|
|
39
|
+
|
|
40
|
+
def initialize(project_dir)
|
|
41
|
+
@project_dir = project_dir
|
|
42
|
+
@file_path = File.join(project_dir, ".aidp", "tasklist.jsonl")
|
|
43
|
+
ensure_file_exists
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Create a new task
|
|
47
|
+
def create(description, priority: :medium, session: nil, discovered_during: nil, tags: [])
|
|
48
|
+
validate_description!(description)
|
|
49
|
+
validate_priority!(priority)
|
|
50
|
+
|
|
51
|
+
task = Task.new(
|
|
52
|
+
id: generate_id,
|
|
53
|
+
description: description.strip,
|
|
54
|
+
status: :pending,
|
|
55
|
+
priority: priority,
|
|
56
|
+
created_at: Time.now,
|
|
57
|
+
updated_at: Time.now,
|
|
58
|
+
session: session,
|
|
59
|
+
discovered_during: discovered_during,
|
|
60
|
+
tags: Array(tags)
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
append_task(task)
|
|
64
|
+
Aidp.log_debug("tasklist", "Created task", task_id: task.id, description: task.description)
|
|
65
|
+
task
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Update task status
|
|
69
|
+
def update_status(task_id, new_status, reason: nil)
|
|
70
|
+
validate_status!(new_status)
|
|
71
|
+
task = find(task_id)
|
|
72
|
+
raise TaskNotFoundError, "Task not found: #{task_id}" unless task
|
|
73
|
+
|
|
74
|
+
task.status = new_status
|
|
75
|
+
task.updated_at = Time.now
|
|
76
|
+
|
|
77
|
+
case new_status
|
|
78
|
+
when :in_progress
|
|
79
|
+
task.started_at ||= Time.now
|
|
80
|
+
when :done
|
|
81
|
+
task.completed_at = Time.now
|
|
82
|
+
when :abandoned
|
|
83
|
+
task.abandoned_at = Time.now
|
|
84
|
+
task.abandoned_reason = reason
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
append_task(task)
|
|
88
|
+
Aidp.log_debug("tasklist", "Updated task status", task_id: task.id, status: new_status)
|
|
89
|
+
task
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Query tasks with optional filters
|
|
93
|
+
def all(status: nil, priority: nil, since: nil, tags: nil)
|
|
94
|
+
tasks = load_latest_tasks
|
|
95
|
+
|
|
96
|
+
tasks = tasks.select { |t| t.status == status } if status
|
|
97
|
+
tasks = tasks.select { |t| t.priority == priority } if priority
|
|
98
|
+
tasks = tasks.select { |t| t.created_at >= since } if since
|
|
99
|
+
tasks = tasks.select { |t| (Array(t.tags) & Array(tags)).any? } if tags && !tags.empty?
|
|
100
|
+
|
|
101
|
+
tasks.sort_by(&:created_at).reverse
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Find single task by ID
|
|
105
|
+
def find(task_id)
|
|
106
|
+
all.find { |t| t.id == task_id }
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Query pending tasks (common operation)
|
|
110
|
+
def pending
|
|
111
|
+
all(status: :pending)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Query in-progress tasks
|
|
115
|
+
def in_progress
|
|
116
|
+
all(status: :in_progress)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Count tasks by status
|
|
120
|
+
def counts
|
|
121
|
+
tasks = load_latest_tasks
|
|
122
|
+
{
|
|
123
|
+
total: tasks.size,
|
|
124
|
+
pending: tasks.count { |t| t.status == :pending },
|
|
125
|
+
in_progress: tasks.count { |t| t.status == :in_progress },
|
|
126
|
+
done: tasks.count { |t| t.status == :done },
|
|
127
|
+
abandoned: tasks.count { |t| t.status == :abandoned }
|
|
128
|
+
}
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
private
|
|
132
|
+
|
|
133
|
+
VALID_STATUSES = [:pending, :in_progress, :done, :abandoned].freeze
|
|
134
|
+
VALID_PRIORITIES = [:high, :medium, :low].freeze
|
|
135
|
+
|
|
136
|
+
def append_task(task)
|
|
137
|
+
File.open(@file_path, "a") do |f|
|
|
138
|
+
f.puts serialize_task(task)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def load_latest_tasks
|
|
143
|
+
return [] unless File.exist?(@file_path)
|
|
144
|
+
|
|
145
|
+
tasks_by_id = {}
|
|
146
|
+
|
|
147
|
+
File.readlines(@file_path).each_with_index do |line, index|
|
|
148
|
+
next if line.strip.empty?
|
|
149
|
+
|
|
150
|
+
begin
|
|
151
|
+
data = JSON.parse(line.strip, symbolize_names: true)
|
|
152
|
+
task = deserialize_task(data)
|
|
153
|
+
tasks_by_id[task.id] = task
|
|
154
|
+
rescue JSON::ParserError => e
|
|
155
|
+
Aidp.log_warn("tasklist", "Skipping malformed JSONL line", line_number: index + 1, error: e.message)
|
|
156
|
+
next
|
|
157
|
+
rescue => e
|
|
158
|
+
Aidp.log_warn("tasklist", "Error loading task", line_number: index + 1, error: e.message)
|
|
159
|
+
next
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
tasks_by_id.values
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def serialize_task(task)
|
|
167
|
+
hash = task.to_h
|
|
168
|
+
# Convert Time objects to ISO8601 strings
|
|
169
|
+
hash[:created_at] = hash[:created_at].iso8601 if hash[:created_at]
|
|
170
|
+
hash[:updated_at] = hash[:updated_at].iso8601 if hash[:updated_at]
|
|
171
|
+
hash[:started_at] = hash[:started_at].iso8601 if hash[:started_at]
|
|
172
|
+
hash[:completed_at] = hash[:completed_at].iso8601 if hash[:completed_at]
|
|
173
|
+
hash[:abandoned_at] = hash[:abandoned_at].iso8601 if hash[:abandoned_at]
|
|
174
|
+
JSON.generate(hash)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def deserialize_task(data)
|
|
178
|
+
Task.new(**data.merge(
|
|
179
|
+
status: data[:status]&.to_sym,
|
|
180
|
+
priority: data[:priority]&.to_sym,
|
|
181
|
+
created_at: parse_time(data[:created_at]),
|
|
182
|
+
updated_at: parse_time(data[:updated_at]),
|
|
183
|
+
started_at: parse_time(data[:started_at]),
|
|
184
|
+
completed_at: parse_time(data[:completed_at]),
|
|
185
|
+
abandoned_at: parse_time(data[:abandoned_at]),
|
|
186
|
+
tags: Array(data[:tags])
|
|
187
|
+
))
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def parse_time(time_string)
|
|
191
|
+
return nil if time_string.nil?
|
|
192
|
+
Time.parse(time_string)
|
|
193
|
+
rescue ArgumentError
|
|
194
|
+
nil
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def generate_id
|
|
198
|
+
"task_#{Time.now.to_i}_#{SecureRandom.hex(4)}"
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def ensure_file_exists
|
|
202
|
+
FileUtils.mkdir_p(File.dirname(@file_path))
|
|
203
|
+
FileUtils.touch(@file_path) unless File.exist?(@file_path)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def validate_description!(description)
|
|
207
|
+
raise InvalidTaskError, "Description cannot be empty" if description.nil? || description.strip.empty?
|
|
208
|
+
raise InvalidTaskError, "Description too long (max 200 chars)" if description.length > 200
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def validate_priority!(priority)
|
|
212
|
+
raise InvalidTaskError, "Invalid priority: #{priority}" unless VALID_PRIORITIES.include?(priority)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def validate_status!(status)
|
|
216
|
+
raise InvalidTaskError, "Invalid status: #{status}" unless VALID_STATUSES.include?(status)
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "fileutils"
|
|
4
|
+
require_relative "../prompt_optimization/optimizer"
|
|
4
5
|
|
|
5
6
|
module Aidp
|
|
6
7
|
module Execute
|
|
@@ -9,21 +10,91 @@ module Aidp
|
|
|
9
10
|
# - Read/write PROMPT.md
|
|
10
11
|
# - Check existence
|
|
11
12
|
# - Archive completed prompts
|
|
13
|
+
# - Optionally optimize prompts using intelligent fragment selection (ZFC)
|
|
12
14
|
class PromptManager
|
|
13
15
|
PROMPT_FILENAME = "PROMPT.md"
|
|
14
16
|
ARCHIVE_DIR = ".aidp/prompt_archive"
|
|
15
17
|
|
|
16
|
-
|
|
18
|
+
attr_reader :optimizer, :last_optimization_stats
|
|
19
|
+
|
|
20
|
+
def initialize(project_dir, config: nil)
|
|
17
21
|
@project_dir = project_dir
|
|
18
22
|
@prompt_path = File.join(project_dir, PROMPT_FILENAME)
|
|
19
23
|
@archive_dir = File.join(project_dir, ARCHIVE_DIR)
|
|
24
|
+
@config = config
|
|
25
|
+
@optimizer = nil
|
|
26
|
+
@last_optimization_stats = nil
|
|
27
|
+
|
|
28
|
+
# Initialize optimizer if enabled
|
|
29
|
+
if config&.respond_to?(:prompt_optimization_enabled?) && config.prompt_optimization_enabled?
|
|
30
|
+
@optimizer = Aidp::PromptOptimization::Optimizer.new(
|
|
31
|
+
project_dir: project_dir,
|
|
32
|
+
config: config.prompt_optimization_config
|
|
33
|
+
)
|
|
34
|
+
end
|
|
20
35
|
end
|
|
21
36
|
|
|
22
37
|
# Write content to PROMPT.md
|
|
38
|
+
# If optimization is enabled, stores the content but doesn't write yet
|
|
39
|
+
# (use write_optimized instead)
|
|
23
40
|
def write(content)
|
|
24
41
|
File.write(@prompt_path, content)
|
|
25
42
|
end
|
|
26
43
|
|
|
44
|
+
# Write optimized prompt using intelligent fragment selection
|
|
45
|
+
#
|
|
46
|
+
# Uses Zero Framework Cognition to select only the most relevant fragments
|
|
47
|
+
# from style guides, templates, and source code based on task context.
|
|
48
|
+
#
|
|
49
|
+
# @param task_context [Hash] Context about the current task
|
|
50
|
+
# @option task_context [Symbol] :task_type Type of task (:feature, :bugfix, etc.)
|
|
51
|
+
# @option task_context [String] :description Task description
|
|
52
|
+
# @option task_context [Array<String>] :affected_files Files being modified
|
|
53
|
+
# @option task_context [String] :step_name Current work loop step
|
|
54
|
+
# @option task_context [Array<String>] :tags Additional context tags
|
|
55
|
+
# @param options [Hash] Optimization options
|
|
56
|
+
# @option options [Boolean] :include_metadata Include debug metadata
|
|
57
|
+
# @return [Boolean] True if optimization was used, false if fallback to regular write
|
|
58
|
+
def write_optimized(task_context, options = {})
|
|
59
|
+
unless @optimizer
|
|
60
|
+
Aidp.logger.warn("prompt_manager", "Optimization requested but not enabled")
|
|
61
|
+
return false
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
begin
|
|
65
|
+
# Use optimizer to build intelligent prompt
|
|
66
|
+
result = @optimizer.optimize_prompt(
|
|
67
|
+
task_type: task_context[:task_type],
|
|
68
|
+
description: task_context[:description],
|
|
69
|
+
affected_files: task_context[:affected_files] || [],
|
|
70
|
+
step_name: task_context[:step_name],
|
|
71
|
+
tags: task_context[:tags] || [],
|
|
72
|
+
options: options
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
# Write optimized prompt
|
|
76
|
+
result.write_to_file(@prompt_path)
|
|
77
|
+
|
|
78
|
+
# Store statistics for inspection
|
|
79
|
+
@last_optimization_stats = result.composition_result
|
|
80
|
+
|
|
81
|
+
# Log optimization results
|
|
82
|
+
Aidp.logger.info(
|
|
83
|
+
"prompt_manager",
|
|
84
|
+
"Optimized prompt written",
|
|
85
|
+
selected_fragments: result.composition_result.selected_count,
|
|
86
|
+
excluded_fragments: result.composition_result.excluded_count,
|
|
87
|
+
tokens: result.estimated_tokens,
|
|
88
|
+
budget_utilization: result.composition_result.budget_utilization
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
true
|
|
92
|
+
rescue => e
|
|
93
|
+
Aidp.logger.error("prompt_manager", "Optimization failed, using fallback", error: e.message)
|
|
94
|
+
false
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
27
98
|
# Read content from PROMPT.md
|
|
28
99
|
def read
|
|
29
100
|
return nil unless exists?
|
|
@@ -57,6 +128,62 @@ module Aidp
|
|
|
57
128
|
def path
|
|
58
129
|
@prompt_path
|
|
59
130
|
end
|
|
131
|
+
|
|
132
|
+
# Get optimization report for last optimization
|
|
133
|
+
#
|
|
134
|
+
# @return [String, nil] Markdown report or nil if no optimization performed
|
|
135
|
+
def optimization_report
|
|
136
|
+
return nil unless @last_optimization_stats
|
|
137
|
+
|
|
138
|
+
# Build report from composition result
|
|
139
|
+
lines = []
|
|
140
|
+
lines << "# Prompt Optimization Report"
|
|
141
|
+
lines << ""
|
|
142
|
+
lines << "## Statistics"
|
|
143
|
+
lines << "- **Selected Fragments**: #{@last_optimization_stats.selected_count}"
|
|
144
|
+
lines << "- **Excluded Fragments**: #{@last_optimization_stats.excluded_count}"
|
|
145
|
+
lines << "- **Total Tokens**: #{@last_optimization_stats.total_tokens} / #{@last_optimization_stats.budget}"
|
|
146
|
+
lines << "- **Budget Utilization**: #{@last_optimization_stats.budget_utilization.round(1)}%"
|
|
147
|
+
lines << "- **Average Relevance Score**: #{(@last_optimization_stats.average_score * 100).round(1)}%"
|
|
148
|
+
lines << ""
|
|
149
|
+
lines << "## Selected Fragments"
|
|
150
|
+
@last_optimization_stats.selected_fragments.each do |scored|
|
|
151
|
+
fragment = scored[:fragment]
|
|
152
|
+
score = scored[:score]
|
|
153
|
+
lines << "- #{fragment_name(fragment)} (#{(score * 100).round(0)}%)"
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
lines.join("\n")
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Check if optimization is enabled
|
|
160
|
+
#
|
|
161
|
+
# @return [Boolean] True if optimizer is available
|
|
162
|
+
def optimization_enabled?
|
|
163
|
+
!@optimizer.nil?
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Get optimizer statistics
|
|
167
|
+
#
|
|
168
|
+
# @return [Hash, nil] Statistics hash or nil if optimizer not available
|
|
169
|
+
def optimizer_stats
|
|
170
|
+
@optimizer&.statistics
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
private
|
|
174
|
+
|
|
175
|
+
# Get human-readable name for a fragment
|
|
176
|
+
def fragment_name(fragment)
|
|
177
|
+
if fragment.respond_to?(:heading)
|
|
178
|
+
fragment.heading
|
|
179
|
+
elsif fragment.respond_to?(:name)
|
|
180
|
+
fragment.name
|
|
181
|
+
elsif fragment.respond_to?(:id)
|
|
182
|
+
fragment.id
|
|
183
|
+
else
|
|
184
|
+
"Unknown fragment"
|
|
185
|
+
end
|
|
186
|
+
end
|
|
60
187
|
end
|
|
61
188
|
end
|
|
62
189
|
end
|