ariadna 1.3.0 → 2.0.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/ariadna.gemspec +0 -1
- data/data/agents/ariadna-codebase-mapper.md +34 -722
- data/data/agents/ariadna-debugger.md +44 -1139
- data/data/agents/ariadna-executor.md +75 -396
- data/data/agents/ariadna-planner.md +78 -1215
- data/data/agents/ariadna-roadmapper.md +55 -582
- data/data/agents/ariadna-verifier.md +60 -702
- data/data/ariadna/templates/config.json +8 -33
- data/data/ariadna/workflows/debug.md +28 -0
- data/data/ariadna/workflows/execute-phase.md +31 -513
- data/data/ariadna/workflows/map-codebase.md +20 -319
- data/data/ariadna/workflows/new-milestone.md +20 -365
- data/data/ariadna/workflows/new-project.md +19 -880
- data/data/ariadna/workflows/plan-phase.md +24 -443
- data/data/ariadna/workflows/progress.md +20 -376
- data/data/ariadna/workflows/quick.md +19 -221
- data/data/ariadna/workflows/roadmap-ops.md +28 -0
- data/data/ariadna/workflows/verify-work.md +23 -560
- data/data/commands/ariadna/add-phase.md +11 -22
- data/data/commands/ariadna/debug.md +11 -143
- data/data/commands/ariadna/execute-phase.md +12 -30
- data/data/commands/ariadna/insert-phase.md +7 -14
- data/data/commands/ariadna/map-codebase.md +16 -49
- data/data/commands/ariadna/new-milestone.md +12 -25
- data/data/commands/ariadna/new-project.md +22 -26
- data/data/commands/ariadna/plan-phase.md +13 -22
- data/data/commands/ariadna/progress.md +16 -6
- data/data/commands/ariadna/quick.md +9 -11
- data/data/commands/ariadna/remove-phase.md +9 -12
- data/data/commands/ariadna/verify-work.md +14 -19
- data/data/skills/rails-backend/API.md +138 -0
- data/data/skills/rails-backend/CONTROLLERS.md +154 -0
- data/data/skills/rails-backend/JOBS.md +132 -0
- data/data/skills/rails-backend/MODELS.md +213 -0
- data/data/skills/rails-backend/SKILL.md +169 -0
- data/data/skills/rails-frontend/ASSETS.md +154 -0
- data/data/skills/rails-frontend/COMPONENTS.md +253 -0
- data/data/skills/rails-frontend/SKILL.md +187 -0
- data/data/skills/rails-frontend/VIEWS.md +168 -0
- data/data/skills/rails-performance/PROFILING.md +106 -0
- data/data/skills/rails-performance/SKILL.md +217 -0
- data/data/skills/rails-security/AUDIT.md +118 -0
- data/data/skills/rails-security/SKILL.md +422 -0
- data/data/skills/rails-testing/FIXTURES.md +78 -0
- data/data/skills/rails-testing/SKILL.md +160 -0
- data/data/skills/rails-testing/SYSTEM-TESTS.md +73 -0
- data/lib/ariadna/installer.rb +11 -15
- data/lib/ariadna/tools/cli.rb +0 -12
- data/lib/ariadna/tools/config_manager.rb +10 -72
- data/lib/ariadna/tools/frontmatter.rb +23 -1
- data/lib/ariadna/tools/init.rb +201 -401
- data/lib/ariadna/tools/model_profiles.rb +6 -14
- data/lib/ariadna/tools/phase_manager.rb +1 -10
- data/lib/ariadna/tools/state_manager.rb +170 -451
- data/lib/ariadna/tools/template_filler.rb +4 -12
- data/lib/ariadna/tools/verification.rb +21 -399
- data/lib/ariadna/uninstaller.rb +9 -0
- data/lib/ariadna/version.rb +1 -1
- data/lib/ariadna.rb +1 -0
- metadata +20 -91
- data/data/agents/ariadna-backend-executor.md +0 -261
- data/data/agents/ariadna-frontend-executor.md +0 -259
- data/data/agents/ariadna-integration-checker.md +0 -418
- data/data/agents/ariadna-phase-researcher.md +0 -469
- data/data/agents/ariadna-plan-checker.md +0 -622
- data/data/agents/ariadna-project-researcher.md +0 -618
- data/data/agents/ariadna-research-synthesizer.md +0 -236
- data/data/agents/ariadna-test-executor.md +0 -266
- data/data/ariadna/references/checkpoints.md +0 -772
- data/data/ariadna/references/continuation-format.md +0 -249
- data/data/ariadna/references/decimal-phase-calculation.md +0 -65
- data/data/ariadna/references/git-integration.md +0 -248
- data/data/ariadna/references/git-planning-commit.md +0 -38
- data/data/ariadna/references/model-profile-resolution.md +0 -32
- data/data/ariadna/references/model-profiles.md +0 -73
- data/data/ariadna/references/phase-argument-parsing.md +0 -61
- data/data/ariadna/references/planning-config.md +0 -194
- data/data/ariadna/references/questioning.md +0 -153
- data/data/ariadna/references/rails-conventions.md +0 -416
- data/data/ariadna/references/tdd.md +0 -267
- data/data/ariadna/references/ui-brand.md +0 -160
- data/data/ariadna/references/verification-patterns.md +0 -853
- data/data/ariadna/templates/codebase/architecture.md +0 -481
- data/data/ariadna/templates/codebase/concerns.md +0 -380
- data/data/ariadna/templates/codebase/conventions.md +0 -434
- data/data/ariadna/templates/codebase/integrations.md +0 -328
- data/data/ariadna/templates/codebase/stack.md +0 -189
- data/data/ariadna/templates/codebase/structure.md +0 -418
- data/data/ariadna/templates/codebase/testing.md +0 -606
- data/data/ariadna/templates/context.md +0 -283
- data/data/ariadna/templates/continue-here.md +0 -78
- data/data/ariadna/templates/debug-subagent-prompt.md +0 -91
- data/data/ariadna/templates/phase-prompt.md +0 -609
- data/data/ariadna/templates/planner-subagent-prompt.md +0 -117
- data/data/ariadna/templates/research-project/ARCHITECTURE.md +0 -439
- data/data/ariadna/templates/research-project/FEATURES.md +0 -168
- data/data/ariadna/templates/research-project/PITFALLS.md +0 -406
- data/data/ariadna/templates/research-project/STACK.md +0 -251
- data/data/ariadna/templates/research-project/SUMMARY.md +0 -247
- data/data/ariadna/templates/state.md +0 -176
- data/data/ariadna/templates/summary-complex.md +0 -59
- data/data/ariadna/templates/summary-minimal.md +0 -41
- data/data/ariadna/templates/summary-standard.md +0 -48
- data/data/ariadna/templates/user-setup.md +0 -310
- data/data/ariadna/workflows/add-phase.md +0 -111
- data/data/ariadna/workflows/add-todo.md +0 -157
- data/data/ariadna/workflows/audit-milestone.md +0 -241
- data/data/ariadna/workflows/check-todos.md +0 -176
- data/data/ariadna/workflows/complete-milestone.md +0 -644
- data/data/ariadna/workflows/diagnose-issues.md +0 -219
- data/data/ariadna/workflows/discovery-phase.md +0 -289
- data/data/ariadna/workflows/discuss-phase.md +0 -408
- data/data/ariadna/workflows/execute-plan.md +0 -448
- data/data/ariadna/workflows/help.md +0 -470
- data/data/ariadna/workflows/insert-phase.md +0 -129
- data/data/ariadna/workflows/list-phase-assumptions.md +0 -178
- data/data/ariadna/workflows/pause-work.md +0 -122
- data/data/ariadna/workflows/plan-milestone-gaps.md +0 -256
- data/data/ariadna/workflows/remove-phase.md +0 -154
- data/data/ariadna/workflows/research-phase.md +0 -74
- data/data/ariadna/workflows/resume-project.md +0 -306
- data/data/ariadna/workflows/set-profile.md +0 -80
- data/data/ariadna/workflows/settings.md +0 -145
- data/data/ariadna/workflows/transition.md +0 -493
- data/data/ariadna/workflows/update.md +0 -212
- data/data/ariadna/workflows/verify-phase.md +0 -226
- data/data/commands/ariadna/add-todo.md +0 -42
- data/data/commands/ariadna/audit-milestone.md +0 -42
- data/data/commands/ariadna/check-todos.md +0 -41
- data/data/commands/ariadna/complete-milestone.md +0 -136
- data/data/commands/ariadna/discuss-phase.md +0 -86
- data/data/commands/ariadna/help.md +0 -22
- data/data/commands/ariadna/list-phase-assumptions.md +0 -50
- data/data/commands/ariadna/pause-work.md +0 -35
- data/data/commands/ariadna/plan-milestone-gaps.md +0 -40
- data/data/commands/ariadna/reapply-patches.md +0 -110
- data/data/commands/ariadna/research-phase.md +0 -187
- data/data/commands/ariadna/resume-work.md +0 -40
- data/data/commands/ariadna/set-profile.md +0 -34
- data/data/commands/ariadna/settings.md +0 -36
- data/data/commands/ariadna/update.md +0 -37
- data/data/guides/backend.md +0 -3069
- data/data/guides/frontend.md +0 -1479
- data/data/guides/performance.md +0 -1193
- data/data/guides/security.md +0 -1522
- data/data/guides/style-guide.md +0 -1091
- data/data/guides/testing.md +0 -504
- data/data/templates.md +0 -94
|
@@ -1,465 +1,199 @@
|
|
|
1
1
|
require "json"
|
|
2
2
|
require_relative "output"
|
|
3
|
-
require_relative "config_manager"
|
|
4
3
|
require_relative "frontmatter"
|
|
5
4
|
|
|
6
5
|
module Ariadna
|
|
7
6
|
module Tools
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
# Memory-directory-based state management using Memory Tool verbs.
|
|
8
|
+
module StateManager # rubocop:disable Metrics/ModuleLength
|
|
9
|
+
MEMORY_DIR = ".ariadna_planning/memory".freeze
|
|
10
|
+
|
|
11
|
+
def self.dispatch(args, raw: false) # rubocop:disable Metrics/CyclomaticComplexity
|
|
12
|
+
subcommand = args.shift
|
|
11
13
|
case subcommand
|
|
12
|
-
when "
|
|
13
|
-
|
|
14
|
-
when "
|
|
15
|
-
|
|
16
|
-
when "
|
|
17
|
-
|
|
18
|
-
when "
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
when "
|
|
22
|
-
|
|
23
|
-
when "
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
14
|
+
when "list" then list_memory(args, raw: raw)
|
|
15
|
+
when "view" then view(args, raw: raw)
|
|
16
|
+
when "create" then create(args, raw: raw)
|
|
17
|
+
when "update" then update(args, raw: raw)
|
|
18
|
+
when "insert" then insert(args, raw: raw)
|
|
19
|
+
when "delete" then delete_file(args, raw: raw)
|
|
20
|
+
when "add-decision" then add_decision(args, raw: raw)
|
|
21
|
+
when "add-blocker" then add_blocker(args, raw: raw)
|
|
22
|
+
when "resolve-blocker" then resolve_blocker(args, raw: raw)
|
|
23
|
+
when "record-metric" then record_metric(args, raw: raw)
|
|
24
|
+
when "record-session" then record_session(args, raw: raw)
|
|
25
|
+
when "history-digest" then history_digest(args, raw: raw)
|
|
26
|
+
else Output.error("Unknown state subcommand: #{subcommand}")
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.list_memory(_args, raw: false)
|
|
31
|
+
dir = memory_path
|
|
32
|
+
FileUtils.mkdir_p(dir)
|
|
33
|
+
files = Dir.children(dir).sort.map do |name|
|
|
34
|
+
{ name: name, size: File.stat(File.join(dir, name)).size }
|
|
35
|
+
end
|
|
36
|
+
Output.json({ files: files }, raw: raw)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.view(args, raw: false)
|
|
40
|
+
filename = args.shift
|
|
41
|
+
Output.error("filename required") unless filename
|
|
42
|
+
path = safe_path!(filename)
|
|
43
|
+
Output.error("File not found: #{filename}") unless File.exist?(path)
|
|
44
|
+
content = File.read(path)
|
|
45
|
+
Output.json({ file: filename, content: content, lines: content.lines.count }, raw: raw)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def self.create(args, raw: false)
|
|
49
|
+
filename = args.shift
|
|
50
|
+
content = args.shift || ""
|
|
51
|
+
Output.error("filename required") unless filename
|
|
52
|
+
path = safe_path!(filename)
|
|
53
|
+
Output.error("File already exists: #{filename}") if File.exist?(path)
|
|
54
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
55
|
+
File.write(path, content)
|
|
56
|
+
Output.json({ created: true, file: filename }, raw: raw)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def self.update(args, raw: false)
|
|
60
|
+
filename, old_str, new_str = args.shift(3)
|
|
61
|
+
Output.error("filename, old_str, and new_str required") unless filename && old_str && new_str
|
|
62
|
+
path = safe_path!(filename)
|
|
63
|
+
Output.error("File not found: #{filename}") unless File.exist?(path)
|
|
64
|
+
content = File.read(path)
|
|
65
|
+
count = content.scan(old_str).length
|
|
66
|
+
Output.error("old_str not found in #{filename}") if count.zero?
|
|
67
|
+
Output.error("old_str is ambiguous (#{count} occurrences) in #{filename}") if count > 1
|
|
68
|
+
File.write(path, content.sub(old_str, new_str))
|
|
69
|
+
Output.json({ updated: true, file: filename }, raw: raw)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def self.insert(args, raw: false)
|
|
73
|
+
filename = args.shift
|
|
74
|
+
line_num = args.shift&.to_i
|
|
75
|
+
text = args.shift
|
|
76
|
+
Output.error("filename, line_number, and text required") unless filename && line_num && text
|
|
77
|
+
path = safe_path!(filename)
|
|
78
|
+
Output.error("File not found: #{filename}") unless File.exist?(path)
|
|
79
|
+
lines = File.readlines(path)
|
|
80
|
+
lines.insert([line_num - 1, 0].max, "#{text}\n")
|
|
81
|
+
File.write(path, lines.join)
|
|
82
|
+
Output.json({ inserted: true, file: filename, at_line: line_num }, raw: raw)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def self.delete_file(args, raw: false)
|
|
86
|
+
filename = args.shift
|
|
87
|
+
Output.error("filename required") unless filename
|
|
88
|
+
path = safe_path!(filename)
|
|
89
|
+
Output.error("File not found: #{filename}") unless File.exist?(path)
|
|
90
|
+
File.delete(path)
|
|
91
|
+
Output.json({ deleted: true, file: filename }, raw: raw)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def self.add_decision(args, raw: false)
|
|
95
|
+
opts = parse_named_args(args, %w[phase summary rationale])
|
|
96
|
+
summary = opts["summary"]
|
|
97
|
+
Output.error("--summary required") unless summary
|
|
98
|
+
phase = opts["phase"] || "?"
|
|
99
|
+
suffix = opts["rationale"] ? " -- #{opts['rationale']}" : ""
|
|
100
|
+
entry = "- [Phase #{phase}]: #{summary}#{suffix}"
|
|
101
|
+
append_to_memory_file("decisions.md", "# Decisions", entry)
|
|
102
|
+
Output.json({ added: true, decision: entry }, raw: raw)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def self.add_blocker(args, raw: false)
|
|
106
|
+
text = extract_flag(args, "--text") || args.first
|
|
107
|
+
Output.error("--text required") unless text
|
|
108
|
+
append_to_memory_file("blockers.md", "# Blockers", "- #{text}")
|
|
109
|
+
Output.json({ added: true, blocker: text }, raw: raw)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def self.resolve_blocker(args, raw: false)
|
|
113
|
+
text = extract_flag(args, "--text") || args.first
|
|
114
|
+
Output.error("--text required") unless text
|
|
115
|
+
path = File.join(memory_path, "blockers.md")
|
|
116
|
+
Output.error("blockers.md not found") unless File.exist?(path)
|
|
117
|
+
lines = File.read(path).split("\n")
|
|
118
|
+
filtered = lines.reject { |l| l.start_with?("- ") && l.downcase.include?(text.downcase) }
|
|
119
|
+
File.write(path, "#{filtered.join("\n")}\n")
|
|
120
|
+
Output.json({ resolved: true, blocker: text }, raw: raw)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def self.record_metric(args, raw: false)
|
|
124
|
+
opts = parse_named_args(args, %w[phase plan duration tasks files])
|
|
125
|
+
phase, plan, duration = opts.values_at("phase", "plan", "duration")
|
|
126
|
+
Output.error("--phase, --plan, and --duration required") unless phase && plan && duration
|
|
127
|
+
tasks = opts["tasks"] || "-"
|
|
128
|
+
files = opts["files"] || "-"
|
|
129
|
+
entry = "| Phase #{phase} P#{plan} | #{duration} | #{tasks} tasks | #{files} files |"
|
|
130
|
+
append_to_memory_file("metrics.md", "# Metrics", entry)
|
|
131
|
+
Output.json({ recorded: true, phase: phase, plan: plan, duration: duration }, raw: raw)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def self.record_session(args, raw: false)
|
|
135
|
+
opts = parse_named_args(args, %w[stopped-at resume-file])
|
|
136
|
+
now = Time.now.utc.iso8601
|
|
137
|
+
lines = ["# Session", "", "**Last session:** #{now}"]
|
|
138
|
+
lines << "**Stopped at:** #{opts['stopped-at']}" if opts["stopped-at"]
|
|
139
|
+
lines << "**Resume file:** #{opts['resume-file']}" if opts["resume-file"]
|
|
140
|
+
path = File.join(memory_path, "session.md")
|
|
141
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
142
|
+
File.write(path, "#{lines.join("\n")}\n")
|
|
143
|
+
Output.json({ recorded: true, timestamp: now }, raw: raw)
|
|
43
144
|
end
|
|
44
145
|
|
|
45
|
-
def self.history_digest(
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
146
|
+
def self.history_digest(_args, raw: false)
|
|
147
|
+
phases_dir = File.join(Dir.pwd, ".ariadna_planning", "phases")
|
|
148
|
+
lines = ["# History Digest", ""]
|
|
149
|
+
collect_phase_summaries(phases_dir, lines) if File.directory?(phases_dir)
|
|
150
|
+
path = File.join(memory_path, "history.md")
|
|
151
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
152
|
+
File.write(path, "#{lines.join("\n")}\n")
|
|
153
|
+
Output.json({ created: true, file: "history.md" }, raw: raw)
|
|
154
|
+
end
|
|
49
155
|
|
|
50
|
-
|
|
51
|
-
Output.json(digest, raw: raw)
|
|
52
|
-
return
|
|
53
|
-
end
|
|
156
|
+
# --- Private helpers ---
|
|
54
157
|
|
|
55
|
-
|
|
158
|
+
def self.collect_phase_summaries(phases_dir, lines) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
|
|
56
159
|
Dir.children(phases_dir).sort.each do |dir|
|
|
57
160
|
dir_path = File.join(phases_dir, dir)
|
|
58
161
|
next unless File.directory?(dir_path)
|
|
59
162
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
fm
|
|
64
|
-
|
|
65
|
-
phase_num = fm["phase"] || dir.split("-").first
|
|
66
|
-
digest[:phases][phase_num] ||= { name: dir.split("-")[1..].join(" ") || "Unknown", provides: [], affects: [], patterns: [] }
|
|
67
|
-
|
|
68
|
-
dep_graph = fm["dependency-graph"] || {}
|
|
69
|
-
(dep_graph["provides"] || fm["provides"] || []).each { |p| digest[:phases][phase_num][:provides] << p }
|
|
70
|
-
(dep_graph["affects"] || []).each { |a| digest[:phases][phase_num][:affects] << a }
|
|
71
|
-
(fm["patterns-established"] || []).each { |p| digest[:phases][phase_num][:patterns] << p }
|
|
72
|
-
(fm["key-decisions"] || []).each { |d| digest[:decisions] << { phase: phase_num, decision: d } }
|
|
73
|
-
|
|
74
|
-
tech = fm["tech-stack"]
|
|
75
|
-
(tech["added"] || []).each { |t| tech_stack_set << (t.is_a?(String) ? t : t["name"]) } if tech.is_a?(Hash)
|
|
163
|
+
lines << "## #{dir}"
|
|
164
|
+
Dir.children(dir_path).select { |f| f.end_with?("SUMMARY.md") }.sort.each do |s|
|
|
165
|
+
fm = Frontmatter.extract(File.read(File.join(dir_path, s)))
|
|
166
|
+
lines << "- **#{s}**: #{(fm['provides'] || []).join(', ')}"
|
|
167
|
+
(fm["key-decisions"] || []).each { |d| lines << " - Decision: #{d}" }
|
|
76
168
|
rescue StandardError
|
|
77
169
|
next
|
|
78
170
|
end
|
|
171
|
+
lines << ""
|
|
79
172
|
end
|
|
80
|
-
|
|
81
|
-
digest[:phases].each_value do |p|
|
|
82
|
-
p[:provides].uniq!
|
|
83
|
-
p[:affects].uniq!
|
|
84
|
-
p[:patterns].uniq!
|
|
85
|
-
end
|
|
86
|
-
digest[:tech_stack] = tech_stack_set.uniq
|
|
87
|
-
|
|
88
|
-
Output.json(digest, raw: raw)
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
def self.summary_extract(argv, raw: false)
|
|
92
|
-
path = argv.shift
|
|
93
|
-
Output.error("path required") unless path
|
|
94
|
-
fields_idx = argv.index("--fields")
|
|
95
|
-
fields = fields_idx ? argv[fields_idx + 1]&.split(",") : nil
|
|
96
|
-
|
|
97
|
-
cwd = Dir.pwd
|
|
98
|
-
full_path = File.expand_path(path, cwd)
|
|
99
|
-
content = File.read(full_path)
|
|
100
|
-
fm = Frontmatter.extract(content)
|
|
101
|
-
|
|
102
|
-
result = fields ? fm.slice(*fields) : fm
|
|
103
|
-
Output.json(result, raw: raw)
|
|
104
|
-
rescue Errno::ENOENT
|
|
105
|
-
Output.error("File not found: #{path}")
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
def self.snapshot(_argv, raw: false)
|
|
109
|
-
cwd = Dir.pwd
|
|
110
|
-
state_path = File.join(cwd, ".ariadna_planning", "STATE.md")
|
|
111
|
-
Output.error("STATE.md not found") unless File.exist?(state_path)
|
|
112
|
-
|
|
113
|
-
content = File.read(state_path)
|
|
114
|
-
fields = {}
|
|
115
|
-
content.scan(/\*\*(.+?):\*\*\s*(.+)/) { |k, v| fields[k.strip] = v.strip }
|
|
116
|
-
|
|
117
|
-
Output.json(fields, raw: raw)
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
# --- Core state operations ---
|
|
121
|
-
|
|
122
|
-
def self.load_state(raw: false)
|
|
123
|
-
cwd = Dir.pwd
|
|
124
|
-
config = ConfigManager.load_config(cwd)
|
|
125
|
-
planning_dir = File.join(cwd, ".ariadna_planning")
|
|
126
|
-
|
|
127
|
-
state_raw = begin
|
|
128
|
-
File.read(File.join(planning_dir, "STATE.md"))
|
|
129
|
-
rescue Errno::ENOENT
|
|
130
|
-
""
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
result = {
|
|
134
|
-
config: config,
|
|
135
|
-
state_raw: state_raw,
|
|
136
|
-
state_exists: !state_raw.empty?,
|
|
137
|
-
roadmap_exists: File.exist?(File.join(planning_dir, "ROADMAP.md")),
|
|
138
|
-
config_exists: File.exist?(File.join(planning_dir, "config.json"))
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
if raw
|
|
142
|
-
lines = config.map { |k, v| "#{k}=#{v}" }
|
|
143
|
-
lines << "config_exists=#{result[:config_exists]}"
|
|
144
|
-
lines << "roadmap_exists=#{result[:roadmap_exists]}"
|
|
145
|
-
lines << "state_exists=#{result[:state_exists]}"
|
|
146
|
-
$stdout.write(lines.join("\n"))
|
|
147
|
-
exit 0
|
|
148
|
-
end
|
|
149
|
-
|
|
150
|
-
Output.json(result)
|
|
151
|
-
end
|
|
152
|
-
|
|
153
|
-
def self.get(section, raw: false)
|
|
154
|
-
cwd = Dir.pwd
|
|
155
|
-
state_path = File.join(cwd, ".ariadna_planning", "STATE.md")
|
|
156
|
-
content = File.read(state_path)
|
|
157
|
-
|
|
158
|
-
unless section
|
|
159
|
-
Output.json({ content: content }, raw: raw, raw_value: content)
|
|
160
|
-
return
|
|
161
|
-
end
|
|
162
|
-
|
|
163
|
-
escaped = Regexp.escape(section)
|
|
164
|
-
|
|
165
|
-
# Check for **field:** value
|
|
166
|
-
if (match = content.match(/\*\*#{escaped}:\*\*\s*(.*)/i))
|
|
167
|
-
Output.json({ section => match[1].strip }, raw: raw, raw_value: match[1].strip)
|
|
168
|
-
return
|
|
169
|
-
end
|
|
170
|
-
|
|
171
|
-
# Check for ## Section
|
|
172
|
-
if (match = content.match(/##\s*#{escaped}\s*\n([\s\S]*?)(?=\n##|$)/i))
|
|
173
|
-
Output.json({ section => match[1].strip }, raw: raw, raw_value: match[1].strip)
|
|
174
|
-
return
|
|
175
|
-
end
|
|
176
|
-
|
|
177
|
-
Output.json({ error: "Section or field \"#{section}\" not found" }, raw: raw, raw_value: "")
|
|
178
|
-
rescue Errno::ENOENT
|
|
179
|
-
Output.error("STATE.md not found")
|
|
180
173
|
end
|
|
181
174
|
|
|
182
|
-
def self.
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
cwd = Dir.pwd
|
|
186
|
-
state_path = File.join(cwd, ".ariadna_planning", "STATE.md")
|
|
187
|
-
content = File.read(state_path)
|
|
188
|
-
|
|
189
|
-
new_content = replace_field(content, field, value)
|
|
190
|
-
if new_content
|
|
191
|
-
File.write(state_path, new_content)
|
|
192
|
-
Output.json({ updated: true })
|
|
193
|
-
else
|
|
194
|
-
Output.json({ updated: false, reason: "Field \"#{field}\" not found in STATE.md" })
|
|
195
|
-
end
|
|
196
|
-
rescue Errno::ENOENT
|
|
197
|
-
Output.json({ updated: false, reason: "STATE.md not found" })
|
|
175
|
+
def self.memory_path
|
|
176
|
+
File.join(Dir.pwd, MEMORY_DIR)
|
|
198
177
|
end
|
|
199
178
|
|
|
200
|
-
def self.
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
results = { updated: [], failed: [] }
|
|
206
|
-
patches.each do |field, value|
|
|
207
|
-
new_content = replace_field(content, field, value)
|
|
208
|
-
if new_content
|
|
209
|
-
content = new_content
|
|
210
|
-
results[:updated] << field
|
|
211
|
-
else
|
|
212
|
-
results[:failed] << field
|
|
213
|
-
end
|
|
214
|
-
end
|
|
215
|
-
|
|
216
|
-
File.write(state_path, content) unless results[:updated].empty?
|
|
217
|
-
Output.json(results, raw: raw, raw_value: results[:updated].empty? ? "false" : "true")
|
|
218
|
-
rescue Errno::ENOENT
|
|
219
|
-
Output.error("STATE.md not found")
|
|
179
|
+
def self.safe_path!(filename)
|
|
180
|
+
dir = memory_path
|
|
181
|
+
resolved = File.expand_path(filename, dir)
|
|
182
|
+
Output.error("Path outside memory directory: #{filename}") unless resolved.start_with?(File.expand_path(dir))
|
|
183
|
+
resolved
|
|
220
184
|
end
|
|
221
185
|
|
|
222
|
-
def self.
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
end
|
|
229
|
-
|
|
230
|
-
content = File.read(state_path)
|
|
231
|
-
current_plan = extract_field(content, "Current Plan")&.to_i
|
|
232
|
-
total_plans = extract_field(content, "Total Plans in Phase")&.to_i
|
|
233
|
-
today = Time.now.utc.strftime("%Y-%m-%d")
|
|
234
|
-
|
|
235
|
-
unless current_plan && total_plans
|
|
236
|
-
Output.json({ error: "Cannot parse Current Plan or Total Plans in Phase from STATE.md" }, raw: raw)
|
|
237
|
-
return
|
|
238
|
-
end
|
|
239
|
-
|
|
240
|
-
if current_plan >= total_plans
|
|
241
|
-
content = replace_field(content, "Status", "Phase complete — ready for verification") || content
|
|
242
|
-
content = replace_field(content, "Last Activity", today) || content
|
|
243
|
-
File.write(state_path, content)
|
|
244
|
-
Output.json({ advanced: false, reason: "last_plan", current_plan: current_plan, total_plans: total_plans }, raw: raw, raw_value: "false")
|
|
186
|
+
def self.append_to_memory_file(filename, header, entry)
|
|
187
|
+
dir = memory_path
|
|
188
|
+
FileUtils.mkdir_p(dir)
|
|
189
|
+
path = File.join(dir, filename)
|
|
190
|
+
if File.exist?(path)
|
|
191
|
+
File.write(path, "#{File.read(path).rstrip}\n#{entry}\n")
|
|
245
192
|
else
|
|
246
|
-
|
|
247
|
-
content = replace_field(content, "Current Plan", new_plan.to_s) || content
|
|
248
|
-
content = replace_field(content, "Status", "Ready to execute") || content
|
|
249
|
-
content = replace_field(content, "Last Activity", today) || content
|
|
250
|
-
File.write(state_path, content)
|
|
251
|
-
Output.json({ advanced: true, previous_plan: current_plan, current_plan: new_plan, total_plans: total_plans }, raw: raw, raw_value: "true")
|
|
193
|
+
File.write(path, "#{header}\n\n#{entry}\n")
|
|
252
194
|
end
|
|
253
195
|
end
|
|
254
196
|
|
|
255
|
-
def self.record_metric(options, raw: false)
|
|
256
|
-
cwd = Dir.pwd
|
|
257
|
-
state_path = File.join(cwd, ".ariadna_planning", "STATE.md")
|
|
258
|
-
unless File.exist?(state_path)
|
|
259
|
-
Output.json({ error: "STATE.md not found" }, raw: raw)
|
|
260
|
-
return
|
|
261
|
-
end
|
|
262
|
-
|
|
263
|
-
phase = options["phase"]
|
|
264
|
-
plan = options["plan"]
|
|
265
|
-
duration = options["duration"]
|
|
266
|
-
Output.error("phase, plan, and duration required") unless phase && plan && duration
|
|
267
|
-
|
|
268
|
-
content = File.read(state_path)
|
|
269
|
-
new_row = "| Phase #{phase} P#{plan} | #{duration} | #{options['tasks'] || '-'} tasks | #{options['files'] || '-'} files |"
|
|
270
|
-
|
|
271
|
-
pattern = /(##\s*Performance Metrics[\s\S]*?\n\|[^\n]+\n\|[-|\s]+\n)([\s\S]*?)(?=\n##|\n$|\z)/i
|
|
272
|
-
if (match = content.match(pattern))
|
|
273
|
-
body = match[2].rstrip
|
|
274
|
-
body = body.strip.empty? || body.include?("None yet") ? new_row : "#{body}\n#{new_row}"
|
|
275
|
-
content = content.sub(pattern, "#{match[1]}#{body}\n")
|
|
276
|
-
File.write(state_path, content)
|
|
277
|
-
Output.json({ recorded: true, phase: phase, plan: plan, duration: duration }, raw: raw, raw_value: "true")
|
|
278
|
-
else
|
|
279
|
-
Output.json({ recorded: false, reason: "Performance Metrics section not found" }, raw: raw, raw_value: "false")
|
|
280
|
-
end
|
|
281
|
-
end
|
|
282
|
-
|
|
283
|
-
def self.update_progress(raw: false)
|
|
284
|
-
cwd = Dir.pwd
|
|
285
|
-
state_path = File.join(cwd, ".ariadna_planning", "STATE.md")
|
|
286
|
-
unless File.exist?(state_path)
|
|
287
|
-
Output.json({ error: "STATE.md not found" }, raw: raw)
|
|
288
|
-
return
|
|
289
|
-
end
|
|
290
|
-
|
|
291
|
-
content = File.read(state_path)
|
|
292
|
-
phases_dir = File.join(cwd, ".ariadna_planning", "phases")
|
|
293
|
-
total_plans = 0
|
|
294
|
-
total_summaries = 0
|
|
295
|
-
|
|
296
|
-
if File.directory?(phases_dir)
|
|
297
|
-
Dir.children(phases_dir).each do |dir|
|
|
298
|
-
dir_path = File.join(phases_dir, dir)
|
|
299
|
-
next unless File.directory?(dir_path)
|
|
300
|
-
|
|
301
|
-
files = Dir.children(dir_path)
|
|
302
|
-
total_plans += files.count { |f| f.match?(/-PLAN\.md$/i) }
|
|
303
|
-
total_summaries += files.count { |f| f.match?(/-SUMMARY\.md$/i) }
|
|
304
|
-
end
|
|
305
|
-
end
|
|
306
|
-
|
|
307
|
-
percent = total_plans > 0 ? (total_summaries.to_f / total_plans * 100).round : 0
|
|
308
|
-
bar_width = 10
|
|
309
|
-
filled = (percent.to_f / 100 * bar_width).round
|
|
310
|
-
bar = "\u2588" * filled + "\u2591" * (bar_width - filled)
|
|
311
|
-
progress_str = "[#{bar}] #{percent}%"
|
|
312
|
-
|
|
313
|
-
if content.match?(/\*\*Progress:\*\*/i) || content.match?(/^Progress:/i)
|
|
314
|
-
content = content.sub(/(Progress:\s*).*/i, "\\1#{progress_str}")
|
|
315
|
-
File.write(state_path, content)
|
|
316
|
-
Output.json({ updated: true, percent: percent, completed: total_summaries, total: total_plans, bar: progress_str }, raw: raw, raw_value: progress_str)
|
|
317
|
-
else
|
|
318
|
-
Output.json({ updated: false, reason: "Progress field not found" }, raw: raw, raw_value: "false")
|
|
319
|
-
end
|
|
320
|
-
end
|
|
321
|
-
|
|
322
|
-
def self.add_decision(options, raw: false)
|
|
323
|
-
cwd = Dir.pwd
|
|
324
|
-
state_path = File.join(cwd, ".ariadna_planning", "STATE.md")
|
|
325
|
-
unless File.exist?(state_path)
|
|
326
|
-
Output.json({ error: "STATE.md not found" }, raw: raw)
|
|
327
|
-
return
|
|
328
|
-
end
|
|
329
|
-
|
|
330
|
-
summary = options["summary"]
|
|
331
|
-
Output.error("summary required") unless summary
|
|
332
|
-
|
|
333
|
-
phase = options["phase"] || "?"
|
|
334
|
-
rationale = options["rationale"]
|
|
335
|
-
entry = "- [Phase #{phase}]: #{summary}#{rationale ? " — #{rationale}" : ''}"
|
|
336
|
-
|
|
337
|
-
content = File.read(state_path)
|
|
338
|
-
pattern = /(###?\s*(?:Decisions|Decisions Made|Accumulated.*Decisions)\s*\n)([\s\S]*?)(?=\n###?|\n##[^#]|\z)/i
|
|
339
|
-
|
|
340
|
-
if (match = content.match(pattern))
|
|
341
|
-
body = match[2].gsub(/None yet\.?\s*\n?/i, "").gsub(/No decisions yet\.?\s*\n?/i, "")
|
|
342
|
-
body = "#{body.rstrip}\n#{entry}\n"
|
|
343
|
-
content = content.sub(pattern, "#{match[1]}#{body}")
|
|
344
|
-
File.write(state_path, content)
|
|
345
|
-
Output.json({ added: true, decision: entry }, raw: raw, raw_value: "true")
|
|
346
|
-
else
|
|
347
|
-
Output.json({ added: false, reason: "Decisions section not found" }, raw: raw, raw_value: "false")
|
|
348
|
-
end
|
|
349
|
-
end
|
|
350
|
-
|
|
351
|
-
def self.add_blocker(text, raw: false)
|
|
352
|
-
cwd = Dir.pwd
|
|
353
|
-
state_path = File.join(cwd, ".ariadna_planning", "STATE.md")
|
|
354
|
-
unless File.exist?(state_path)
|
|
355
|
-
Output.json({ error: "STATE.md not found" }, raw: raw)
|
|
356
|
-
return
|
|
357
|
-
end
|
|
358
|
-
Output.error("text required") unless text
|
|
359
|
-
|
|
360
|
-
content = File.read(state_path)
|
|
361
|
-
entry = "- #{text}"
|
|
362
|
-
pattern = /(###?\s*(?:Blockers|Blockers\/Concerns|Concerns)\s*\n)([\s\S]*?)(?=\n###?|\n##[^#]|\z)/i
|
|
363
|
-
|
|
364
|
-
if (match = content.match(pattern))
|
|
365
|
-
body = match[2].gsub(/None\.?\s*\n?/i, "").gsub(/None yet\.?\s*\n?/i, "")
|
|
366
|
-
body = "#{body.rstrip}\n#{entry}\n"
|
|
367
|
-
content = content.sub(pattern, "#{match[1]}#{body}")
|
|
368
|
-
File.write(state_path, content)
|
|
369
|
-
Output.json({ added: true, blocker: text }, raw: raw, raw_value: "true")
|
|
370
|
-
else
|
|
371
|
-
Output.json({ added: false, reason: "Blockers section not found" }, raw: raw, raw_value: "false")
|
|
372
|
-
end
|
|
373
|
-
end
|
|
374
|
-
|
|
375
|
-
def self.resolve_blocker(text, raw: false)
|
|
376
|
-
cwd = Dir.pwd
|
|
377
|
-
state_path = File.join(cwd, ".ariadna_planning", "STATE.md")
|
|
378
|
-
unless File.exist?(state_path)
|
|
379
|
-
Output.json({ error: "STATE.md not found" }, raw: raw)
|
|
380
|
-
return
|
|
381
|
-
end
|
|
382
|
-
Output.error("text required") unless text
|
|
383
|
-
|
|
384
|
-
content = File.read(state_path)
|
|
385
|
-
pattern = /(###?\s*(?:Blockers|Blockers\/Concerns|Concerns)\s*\n)([\s\S]*?)(?=\n###?|\n##[^#]|\z)/i
|
|
386
|
-
|
|
387
|
-
if (match = content.match(pattern))
|
|
388
|
-
lines = match[2].split("\n")
|
|
389
|
-
filtered = lines.reject { |line| line.start_with?("- ") && line.downcase.include?(text.downcase) }
|
|
390
|
-
new_body = filtered.join("\n")
|
|
391
|
-
new_body = "None\n" if new_body.strip.empty? || !new_body.include?("- ")
|
|
392
|
-
content = content.sub(pattern, "#{match[1]}#{new_body}")
|
|
393
|
-
File.write(state_path, content)
|
|
394
|
-
Output.json({ resolved: true, blocker: text }, raw: raw, raw_value: "true")
|
|
395
|
-
else
|
|
396
|
-
Output.json({ resolved: false, reason: "Blockers section not found" }, raw: raw, raw_value: "false")
|
|
397
|
-
end
|
|
398
|
-
end
|
|
399
|
-
|
|
400
|
-
def self.record_session(options, raw: false)
|
|
401
|
-
cwd = Dir.pwd
|
|
402
|
-
state_path = File.join(cwd, ".ariadna_planning", "STATE.md")
|
|
403
|
-
unless File.exist?(state_path)
|
|
404
|
-
Output.json({ error: "STATE.md not found" }, raw: raw)
|
|
405
|
-
return
|
|
406
|
-
end
|
|
407
|
-
|
|
408
|
-
content = File.read(state_path)
|
|
409
|
-
now = Time.now.utc.iso8601
|
|
410
|
-
updated = []
|
|
411
|
-
|
|
412
|
-
new_content = replace_field(content, "Last session", now)
|
|
413
|
-
if new_content
|
|
414
|
-
content = new_content
|
|
415
|
-
updated << "Last session"
|
|
416
|
-
end
|
|
417
|
-
|
|
418
|
-
if options["stopped-at"]
|
|
419
|
-
%w[Stopped At Stopped at].each do |field|
|
|
420
|
-
new_content = replace_field(content, field, options["stopped-at"])
|
|
421
|
-
if new_content
|
|
422
|
-
content = new_content
|
|
423
|
-
updated << "Stopped At"
|
|
424
|
-
break
|
|
425
|
-
end
|
|
426
|
-
end
|
|
427
|
-
end
|
|
428
|
-
|
|
429
|
-
resume = options["resume-file"] || "None"
|
|
430
|
-
%w[Resume\ File Resume\ file].each do |field|
|
|
431
|
-
new_content = replace_field(content, field, resume)
|
|
432
|
-
if new_content
|
|
433
|
-
content = new_content
|
|
434
|
-
updated << "Resume File"
|
|
435
|
-
break
|
|
436
|
-
end
|
|
437
|
-
end
|
|
438
|
-
|
|
439
|
-
if updated.any?
|
|
440
|
-
File.write(state_path, content)
|
|
441
|
-
Output.json({ recorded: true, updated: updated }, raw: raw, raw_value: "true")
|
|
442
|
-
else
|
|
443
|
-
Output.json({ recorded: false, reason: "No session fields found" }, raw: raw, raw_value: "false")
|
|
444
|
-
end
|
|
445
|
-
end
|
|
446
|
-
|
|
447
|
-
# --- Private helpers ---
|
|
448
|
-
|
|
449
|
-
def self.extract_field(content, field_name)
|
|
450
|
-
pattern = /\*\*#{Regexp.escape(field_name)}:\*\*\s*(.+)/i
|
|
451
|
-
match = content.match(pattern)
|
|
452
|
-
match ? match[1].strip : nil
|
|
453
|
-
end
|
|
454
|
-
|
|
455
|
-
def self.replace_field(content, field_name, new_value)
|
|
456
|
-
escaped = Regexp.escape(field_name)
|
|
457
|
-
pattern = /(\*\*#{escaped}:\*\*\s*)(.*)/i
|
|
458
|
-
return nil unless content.match?(pattern)
|
|
459
|
-
|
|
460
|
-
content.sub(pattern, "\\1#{new_value}")
|
|
461
|
-
end
|
|
462
|
-
|
|
463
197
|
def self.extract_flag(argv, flag)
|
|
464
198
|
idx = argv.index(flag)
|
|
465
199
|
return nil unless idx
|
|
@@ -467,32 +201,17 @@ module Ariadna
|
|
|
467
201
|
argv[idx + 1]
|
|
468
202
|
end
|
|
469
203
|
|
|
470
|
-
def self.parse_patches(argv)
|
|
471
|
-
patches = {}
|
|
472
|
-
i = 0
|
|
473
|
-
while i < argv.length
|
|
474
|
-
if argv[i].start_with?("--")
|
|
475
|
-
key = argv[i].sub(/\A--/, "")
|
|
476
|
-
patches[key] = argv[i + 1]
|
|
477
|
-
i += 2
|
|
478
|
-
else
|
|
479
|
-
i += 1
|
|
480
|
-
end
|
|
481
|
-
end
|
|
482
|
-
patches
|
|
483
|
-
end
|
|
484
|
-
|
|
485
204
|
def self.parse_named_args(argv, known_keys)
|
|
486
205
|
result = {}
|
|
487
206
|
known_keys.each do |key|
|
|
488
|
-
|
|
489
|
-
idx = argv.index(flag)
|
|
207
|
+
idx = argv.index("--#{key}")
|
|
490
208
|
result[key] = argv[idx + 1] if idx
|
|
491
209
|
end
|
|
492
210
|
result
|
|
493
211
|
end
|
|
494
212
|
|
|
495
|
-
private_class_method :
|
|
213
|
+
private_class_method :memory_path, :safe_path!, :append_to_memory_file,
|
|
214
|
+
:extract_flag, :parse_named_args, :collect_phase_summaries
|
|
496
215
|
end
|
|
497
216
|
end
|
|
498
217
|
end
|