aidp 0.18.0 → 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/lib/aidp/cli/terminal_io.rb +5 -2
- data/lib/aidp/execute/agent_signal_parser.rb +20 -0
- data/lib/aidp/execute/persistent_tasklist.rb +220 -0
- data/lib/aidp/execute/repl_macros.rb +164 -0
- data/lib/aidp/execute/work_loop_runner.rb +54 -0
- data/lib/aidp/version.rb +1 -1
- data/lib/aidp.rb +1 -0
- metadata +2 -1
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/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
|
|
@@ -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
|
|
@@ -293,6 +293,12 @@ module Aidp
|
|
|
293
293
|
usage: "/prompt <explain|stats|expand|reset>",
|
|
294
294
|
example: "/prompt explain",
|
|
295
295
|
handler: method(:cmd_prompt)
|
|
296
|
+
},
|
|
297
|
+
"/tasks" => {
|
|
298
|
+
description: "Manage persistent tasklist (cross-session task tracking)",
|
|
299
|
+
usage: "/tasks <list|show|done|abandon|stats> [args]",
|
|
300
|
+
example: "/tasks list pending",
|
|
301
|
+
handler: method(:cmd_tasks)
|
|
296
302
|
}
|
|
297
303
|
}
|
|
298
304
|
end
|
|
@@ -2037,6 +2043,164 @@ module Aidp
|
|
|
2037
2043
|
}
|
|
2038
2044
|
end
|
|
2039
2045
|
|
|
2046
|
+
# Manage persistent tasklist
|
|
2047
|
+
def cmd_tasks(args)
|
|
2048
|
+
tasklist = PersistentTasklist.new(@project_dir)
|
|
2049
|
+
subcommand = args[0]
|
|
2050
|
+
|
|
2051
|
+
case subcommand
|
|
2052
|
+
when "list", nil
|
|
2053
|
+
cmd_tasks_list(tasklist, args[1])
|
|
2054
|
+
when "show"
|
|
2055
|
+
cmd_tasks_show(tasklist, args[1])
|
|
2056
|
+
when "done"
|
|
2057
|
+
cmd_tasks_done(tasklist, args[1])
|
|
2058
|
+
when "abandon"
|
|
2059
|
+
cmd_tasks_abandon(tasklist, args[1], args[2..]&.join(" "))
|
|
2060
|
+
when "stats"
|
|
2061
|
+
cmd_tasks_stats(tasklist)
|
|
2062
|
+
else
|
|
2063
|
+
{
|
|
2064
|
+
success: false,
|
|
2065
|
+
message: "Unknown subcommand: #{subcommand}. Use: list, show, done, abandon, stats",
|
|
2066
|
+
action: :none
|
|
2067
|
+
}
|
|
2068
|
+
end
|
|
2069
|
+
rescue => e
|
|
2070
|
+
Aidp.log_error("repl_macros", "Tasks command failed", error: e.message)
|
|
2071
|
+
{success: false, message: "Error: #{e.message}", action: :none}
|
|
2072
|
+
end
|
|
2073
|
+
|
|
2074
|
+
private
|
|
2075
|
+
|
|
2076
|
+
# List tasks with optional status filter
|
|
2077
|
+
def cmd_tasks_list(tasklist, status_filter = nil)
|
|
2078
|
+
status = status_filter&.to_sym
|
|
2079
|
+
tasks = status ? tasklist.all(status: status) : tasklist.all
|
|
2080
|
+
|
|
2081
|
+
if tasks.empty?
|
|
2082
|
+
message = status ? "No #{status} tasks found." : "No tasks found."
|
|
2083
|
+
return {success: true, message: message, action: :display}
|
|
2084
|
+
end
|
|
2085
|
+
|
|
2086
|
+
# Group by status
|
|
2087
|
+
by_status = tasks.group_by(&:status)
|
|
2088
|
+
output = []
|
|
2089
|
+
|
|
2090
|
+
[:pending, :in_progress, :done, :abandoned].each do |st|
|
|
2091
|
+
next unless by_status[st]
|
|
2092
|
+
next if status && st != status # Skip if filtering by specific status
|
|
2093
|
+
|
|
2094
|
+
output << ""
|
|
2095
|
+
output << "#{st.to_s.upcase.tr("_", " ")} (#{by_status[st].size})"
|
|
2096
|
+
output << "=" * 50
|
|
2097
|
+
|
|
2098
|
+
by_status[st].each do |task|
|
|
2099
|
+
priority_icon = case task.priority
|
|
2100
|
+
when :high then "⚠️ "
|
|
2101
|
+
when :medium then "○ "
|
|
2102
|
+
when :low then "· "
|
|
2103
|
+
end
|
|
2104
|
+
|
|
2105
|
+
age = ((Time.now - task.created_at) / 86400).to_i
|
|
2106
|
+
age_str = (age > 0) ? " (#{age}d ago)" : " (today)"
|
|
2107
|
+
|
|
2108
|
+
output << " #{priority_icon}[#{task.id}] #{task.description}#{age_str}"
|
|
2109
|
+
end
|
|
2110
|
+
end
|
|
2111
|
+
|
|
2112
|
+
{
|
|
2113
|
+
success: true,
|
|
2114
|
+
message: output.join("\n"),
|
|
2115
|
+
action: :display
|
|
2116
|
+
}
|
|
2117
|
+
end
|
|
2118
|
+
|
|
2119
|
+
# Show detailed information about a specific task
|
|
2120
|
+
def cmd_tasks_show(tasklist, task_id)
|
|
2121
|
+
return {success: false, message: "Usage: /tasks show <task_id>", action: :none} unless task_id
|
|
2122
|
+
|
|
2123
|
+
task = tasklist.find(task_id)
|
|
2124
|
+
unless task
|
|
2125
|
+
return {success: false, message: "Task not found: #{task_id}", action: :none}
|
|
2126
|
+
end
|
|
2127
|
+
|
|
2128
|
+
output = []
|
|
2129
|
+
output << ""
|
|
2130
|
+
output << "Task Details:"
|
|
2131
|
+
output << "=" * 50
|
|
2132
|
+
output << "ID: #{task.id}"
|
|
2133
|
+
output << "Description: #{task.description}"
|
|
2134
|
+
output << "Status: #{task.status}"
|
|
2135
|
+
output << "Priority: #{task.priority}"
|
|
2136
|
+
output << "Created: #{task.created_at}"
|
|
2137
|
+
output << "Updated: #{task.updated_at}"
|
|
2138
|
+
output << "Session: #{task.session}" if task.session
|
|
2139
|
+
output << "Context: #{task.discovered_during}" if task.discovered_during
|
|
2140
|
+
output << "Started: #{task.started_at}" if task.started_at
|
|
2141
|
+
output << "Completed: #{task.completed_at}" if task.completed_at
|
|
2142
|
+
output << "Abandoned: #{task.abandoned_at} (#{task.abandoned_reason})" if task.abandoned_at
|
|
2143
|
+
output << "Tags: #{task.tags.join(", ")}" if task.tags&.any?
|
|
2144
|
+
|
|
2145
|
+
{
|
|
2146
|
+
success: true,
|
|
2147
|
+
message: output.join("\n"),
|
|
2148
|
+
action: :display
|
|
2149
|
+
}
|
|
2150
|
+
end
|
|
2151
|
+
|
|
2152
|
+
# Mark a task as done
|
|
2153
|
+
def cmd_tasks_done(tasklist, task_id)
|
|
2154
|
+
return {success: false, message: "Usage: /tasks done <task_id>", action: :none} unless task_id
|
|
2155
|
+
|
|
2156
|
+
task = tasklist.update_status(task_id, :done)
|
|
2157
|
+
{
|
|
2158
|
+
success: true,
|
|
2159
|
+
message: "✓ Task marked as done: #{task.description}",
|
|
2160
|
+
action: :display
|
|
2161
|
+
}
|
|
2162
|
+
rescue PersistentTasklist::TaskNotFoundError
|
|
2163
|
+
{success: false, message: "Task not found: #{task_id}", action: :none}
|
|
2164
|
+
end
|
|
2165
|
+
|
|
2166
|
+
# Abandon a task with optional reason
|
|
2167
|
+
def cmd_tasks_abandon(tasklist, task_id, reason = nil)
|
|
2168
|
+
return {success: false, message: "Usage: /tasks abandon <task_id> [reason]", action: :none} unless task_id
|
|
2169
|
+
|
|
2170
|
+
task = tasklist.update_status(task_id, :abandoned, reason: reason)
|
|
2171
|
+
message = "✗ Task abandoned: #{task.description}"
|
|
2172
|
+
message += " (Reason: #{reason})" if reason
|
|
2173
|
+
|
|
2174
|
+
{
|
|
2175
|
+
success: true,
|
|
2176
|
+
message: message,
|
|
2177
|
+
action: :display
|
|
2178
|
+
}
|
|
2179
|
+
rescue PersistentTasklist::TaskNotFoundError
|
|
2180
|
+
{success: false, message: "Task not found: #{task_id}", action: :none}
|
|
2181
|
+
end
|
|
2182
|
+
|
|
2183
|
+
# Show task statistics
|
|
2184
|
+
def cmd_tasks_stats(tasklist)
|
|
2185
|
+
counts = tasklist.counts
|
|
2186
|
+
|
|
2187
|
+
output = []
|
|
2188
|
+
output << ""
|
|
2189
|
+
output << "Task Statistics:"
|
|
2190
|
+
output << "=" * 50
|
|
2191
|
+
output << "Total: #{counts[:total]}"
|
|
2192
|
+
output << "Pending: #{counts[:pending]}"
|
|
2193
|
+
output << "In Progress: #{counts[:in_progress]}"
|
|
2194
|
+
output << "Done: #{counts[:done]}"
|
|
2195
|
+
output << "Abandoned: #{counts[:abandoned]}"
|
|
2196
|
+
|
|
2197
|
+
{
|
|
2198
|
+
success: true,
|
|
2199
|
+
message: output.join("\n"),
|
|
2200
|
+
action: :display
|
|
2201
|
+
}
|
|
2202
|
+
end
|
|
2203
|
+
|
|
2040
2204
|
private
|
|
2041
2205
|
|
|
2042
2206
|
# Load configuration for prompt commands
|
|
@@ -51,6 +51,7 @@ module Aidp
|
|
|
51
51
|
@checkpoint = Checkpoint.new(project_dir)
|
|
52
52
|
@checkpoint_display = CheckpointDisplay.new
|
|
53
53
|
@guard_policy = GuardPolicy.new(project_dir, config.guards_config)
|
|
54
|
+
@persistent_tasklist = PersistentTasklist.new(project_dir)
|
|
54
55
|
@iteration_count = 0
|
|
55
56
|
@step_name = nil
|
|
56
57
|
@options = options
|
|
@@ -74,6 +75,7 @@ module Aidp
|
|
|
74
75
|
display_message(" Flow: Deterministic ↔ Agentic with fix-forward core", type: :info)
|
|
75
76
|
|
|
76
77
|
display_guard_policy_status
|
|
78
|
+
display_pending_tasks
|
|
77
79
|
|
|
78
80
|
@unit_scheduler = WorkLoopUnitScheduler.new(units_config)
|
|
79
81
|
base_context = context.dup
|
|
@@ -148,6 +150,9 @@ module Aidp
|
|
|
148
150
|
transition_to(:apply_patch)
|
|
149
151
|
agent_result = apply_patch
|
|
150
152
|
|
|
153
|
+
# Process agent output for task filing signals
|
|
154
|
+
process_task_filing(agent_result)
|
|
155
|
+
|
|
151
156
|
transition_to(:test)
|
|
152
157
|
test_results = @test_runner.run_tests
|
|
153
158
|
lint_results = @test_runner.run_linters
|
|
@@ -776,6 +781,55 @@ module Aidp
|
|
|
776
781
|
display_message("")
|
|
777
782
|
end
|
|
778
783
|
|
|
784
|
+
# Display pending tasks from persistent tasklist
|
|
785
|
+
def display_pending_tasks
|
|
786
|
+
pending_tasks = @persistent_tasklist.pending
|
|
787
|
+
return if pending_tasks.empty?
|
|
788
|
+
|
|
789
|
+
display_message("\n📋 Pending Tasks from Previous Sessions:", type: :info)
|
|
790
|
+
|
|
791
|
+
# Show up to 5 most recent pending tasks
|
|
792
|
+
pending_tasks.take(5).each do |task|
|
|
793
|
+
priority_icon = case task.priority
|
|
794
|
+
when :high then "⚠️ "
|
|
795
|
+
when :medium then "○ "
|
|
796
|
+
when :low then "· "
|
|
797
|
+
end
|
|
798
|
+
|
|
799
|
+
age = ((Time.now - task.created_at) / 86400).to_i
|
|
800
|
+
age_str = (age > 0) ? " (#{age}d ago)" : " (today)"
|
|
801
|
+
|
|
802
|
+
display_message(" #{priority_icon}#{task.description}#{age_str}", type: :info)
|
|
803
|
+
end
|
|
804
|
+
|
|
805
|
+
if pending_tasks.size > 5
|
|
806
|
+
display_message(" ... and #{pending_tasks.size - 5} more. Use /tasks list to see all", type: :info)
|
|
807
|
+
end
|
|
808
|
+
|
|
809
|
+
display_message("")
|
|
810
|
+
end
|
|
811
|
+
|
|
812
|
+
# Process agent output for task filing signals
|
|
813
|
+
def process_task_filing(agent_result)
|
|
814
|
+
return unless agent_result && agent_result[:output]
|
|
815
|
+
|
|
816
|
+
filed_tasks = AgentSignalParser.parse_task_filing(agent_result[:output])
|
|
817
|
+
return if filed_tasks.empty?
|
|
818
|
+
|
|
819
|
+
filed_tasks.each do |task_data|
|
|
820
|
+
task = @persistent_tasklist.create(
|
|
821
|
+
task_data[:description],
|
|
822
|
+
priority: task_data[:priority],
|
|
823
|
+
session: @step_name,
|
|
824
|
+
discovered_during: "#{@step_name} iteration #{@iteration_count}",
|
|
825
|
+
tags: task_data[:tags]
|
|
826
|
+
)
|
|
827
|
+
|
|
828
|
+
Aidp.log_info("tasklist", "Filed new task from agent", task_id: task.id, description: task.description)
|
|
829
|
+
display_message("📋 Filed task: #{task.description} (#{task.id})", type: :info)
|
|
830
|
+
end
|
|
831
|
+
end
|
|
832
|
+
|
|
779
833
|
# Validate changes against guard policy
|
|
780
834
|
# Returns validation result with errors if any
|
|
781
835
|
def validate_guard_policy(changed_files = [])
|
data/lib/aidp/version.rb
CHANGED
data/lib/aidp.rb
CHANGED
|
@@ -59,6 +59,7 @@ require_relative "aidp/execute/checkpoint"
|
|
|
59
59
|
require_relative "aidp/execute/checkpoint_display"
|
|
60
60
|
require_relative "aidp/execute/work_loop_state"
|
|
61
61
|
require_relative "aidp/execute/instruction_queue"
|
|
62
|
+
require_relative "aidp/execute/persistent_tasklist"
|
|
62
63
|
require_relative "aidp/execute/async_work_loop_runner"
|
|
63
64
|
require_relative "aidp/execute/interactive_repl"
|
|
64
65
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: aidp
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.19.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Bart Agapinan
|
|
@@ -271,6 +271,7 @@ files:
|
|
|
271
271
|
- lib/aidp/execute/guard_policy.rb
|
|
272
272
|
- lib/aidp/execute/instruction_queue.rb
|
|
273
273
|
- lib/aidp/execute/interactive_repl.rb
|
|
274
|
+
- lib/aidp/execute/persistent_tasklist.rb
|
|
274
275
|
- lib/aidp/execute/progress.rb
|
|
275
276
|
- lib/aidp/execute/prompt_manager.rb
|
|
276
277
|
- lib/aidp/execute/repl_macros.rb
|