@zalom/plastic 2.0.0-alpha.2 → 2.0.0-alpha.20
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.
- package/agents/plastic-enforcer.md +3 -1
- package/agents/plastic-executor.md +9 -3
- package/hooks/hooks.json +12 -0
- package/hooks/message-display +134 -0
- package/hooks/statusline +4 -27
- package/package.json +2 -2
- package/scripts/agent-report +8 -2
- package/scripts/dashboard.rb +297 -18
- package/scripts/doctor.rb +652 -5
- package/scripts/end-intent +219 -0
- package/scripts/hook-capture +25 -113
- package/scripts/hook-message-display +81 -0
- package/scripts/hook-record +12 -4
- package/scripts/hook-session-start +23 -1
- package/scripts/index-projection +74 -0
- package/scripts/intent-screen +77 -0
- package/scripts/lib/action_graph_shim.rb +277 -0
- package/scripts/lib/arm.rb +26 -1
- package/scripts/lib/atomic_write.rb +31 -0
- package/scripts/lib/core_integrity.rb +71 -0
- package/scripts/lib/dashboard_screen.rb +40 -0
- package/scripts/lib/day_summary.rb +7 -2
- package/scripts/lib/doctor_core.rb +101 -5
- package/scripts/lib/doctor_session_ledger.rb +52 -0
- package/scripts/lib/graph_edges.rb +121 -0
- package/scripts/lib/graph_file.rb +246 -0
- package/scripts/lib/graph_tree.rb +98 -0
- package/scripts/lib/guarded_append.rb +155 -0
- package/scripts/lib/hook_registry.rb +14 -0
- package/scripts/lib/hook_replay.rb +211 -0
- package/scripts/lib/index_projection.rb +201 -0
- package/scripts/lib/installer_core.rb +131 -4
- package/scripts/lib/intent_screen.rb +309 -0
- package/scripts/lib/intent_screen_ansi.rb +262 -0
- package/scripts/lib/message_display.rb +586 -0
- package/scripts/lib/node_file.rb +214 -0
- package/scripts/lib/node_ids.rb +99 -0
- package/scripts/lib/node_ledger.rb +377 -0
- package/scripts/lib/node_packet.rb +886 -0
- package/scripts/lib/node_return.rb +199 -0
- package/scripts/lib/node_worktree.rb +337 -0
- package/scripts/lib/outcome_report.rb +440 -0
- package/scripts/lib/packet_wrapper.rb +132 -0
- package/scripts/lib/ready_set.rb +462 -0
- package/scripts/lib/release_guard.rb +16 -0
- package/scripts/lib/report_screen.rb +1967 -0
- package/scripts/lib/roadmap_graph.rb +210 -0
- package/scripts/lib/roadmap_migration.rb +95 -0
- package/scripts/lib/roadmap_queue.rb +155 -5
- package/scripts/lib/roadmap_render.rb +150 -0
- package/scripts/lib/roadmap_savepoint.rb +62 -12
- package/scripts/lib/runner_absorb.rb +620 -0
- package/scripts/lib/runner_answer.rb +206 -0
- package/scripts/lib/runner_core.rb +194 -0
- package/scripts/lib/runner_dispatch.rb +482 -0
- package/scripts/lib/runner_policy.rb +142 -0
- package/scripts/lib/runner_proposals.rb +254 -0
- package/scripts/lib/runner_rewind.rb +201 -0
- package/scripts/lib/runner_sweep.rb +231 -0
- package/scripts/lib/savepoint.rb +149 -12
- package/scripts/lib/screen_paint.rb +555 -0
- package/scripts/lib/screens/dashboard.rb +20 -0
- package/scripts/lib/screens/plan.rb +18 -0
- package/scripts/lib/screens/roadmap.rb +15 -0
- package/scripts/lib/session_git.rb +49 -18
- package/scripts/lib/session_ledger.rb +128 -0
- package/scripts/lib/verify_intent.rb +33 -0
- package/scripts/lib/work_graph_validator.rb +201 -0
- package/scripts/node-packet +92 -0
- package/scripts/node-transition +291 -0
- package/scripts/outcome-report +74 -0
- package/scripts/plastic-lock +8 -1
- package/scripts/ready-set +126 -0
- package/scripts/release-check +118 -0
- package/scripts/report-screen +281 -0
- package/scripts/roadmap-graph +119 -0
- package/scripts/roadmap-savepoint +7 -0
- package/scripts/runner +392 -0
- package/scripts/savepoint-note +69 -0
- package/scripts/spawn-preamble +9 -2
- package/scripts/validate-work-graph +39 -0
- package/scripts/verify-intent +2 -1
- package/skills/auto/SKILL.md +24 -17
- package/skills/auto/references/human-report-contract.md +136 -54
- package/skills/conventions/references/locks-and-worktrees.md +12 -0
- package/skills/dashboard/SKILL.md +13 -2
- package/skills/dashboard/templates/dashboard-global.md +1 -1
- package/skills/dashboard/templates/dashboard-project.md +2 -2
- package/skills/doctor/SKILL.md +10 -4
- package/skills/intent-continuing/SKILL.md +51 -41
- package/skills/intent-continuing/references/board-fill.md +9 -0
- package/skills/intent-continuing/references/boarding-matrix.md +6 -5
- package/skills/intent-continuing/references/context-management.md +1 -1
- package/skills/intent-ending/SKILL.md +36 -16
- package/skills/intent-executing/SKILL.md +17 -5
- package/skills/intent-executing/implementer-prompt.md +6 -1
- package/skills/intent-speccing/SKILL.md +7 -4
- package/skills/releasing/SKILL.md +39 -0
- package/skills/releasing/references/promotion-and-tagging.md +10 -6
- package/skills/releasing/references/release-lines.md +1 -1
- package/skills/roadmap/SKILL.md +26 -0
- package/skills/roadmap/references/file-format.md +10 -0
- package/templates/dashboard-screen.md +22 -0
- package/templates/display-fixture.md +21 -0
- package/templates/graph.md +16 -0
- package/templates/intent-screen.md +17 -0
- package/templates/node-decision.md +11 -0
- package/templates/node-research.md +11 -0
- package/templates/node-verify.md +13 -0
- package/templates/node-work.md +22 -0
- package/templates/outcome.md +19 -1
- package/templates/report-plan.md +15 -0
- package/templates/report-roadmap-delivered.md +10 -0
- package/templates/report-roadmap-plan.md +9 -0
- package/templates/report-roadmap-state.md +9 -0
- package/templates/report-state.md +11 -0
- package/templates/roadmap.md +13 -0
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# encoding: UTF-8
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
# report-screen - the pre-delivery plan (intent 331b), the four delivery
|
|
6
|
+
# reports: mid-delivery state, post-delivery delivered, and delay (intent
|
|
7
|
+
# 317), plus session, the whole of one session's delivered work followed by
|
|
8
|
+
# the roster (intent 330). Each fills from the record via
|
|
9
|
+
# scripts/lib/report_screen.rb; no number here is written by eye.
|
|
10
|
+
#
|
|
11
|
+
# Usage:
|
|
12
|
+
# report-screen plan <intent_dir> [--ansi]
|
|
13
|
+
# report-screen state <intent_dir> [--changed "<text>"] [--ansi]
|
|
14
|
+
# report-screen state --all <store_root> [--changed "<text>"] [--ansi]
|
|
15
|
+
# report-screen delivered <intent_dir> [--ansi] [--repo <dir>]
|
|
16
|
+
# report-screen delay <intent_dir> [--ansi]
|
|
17
|
+
# report-screen session <tier_root> [--session <id>] [--since <iso>]
|
|
18
|
+
# [--ledger-root <dir>] [--ansi]
|
|
19
|
+
# report-screen roadmap <roadmap.md> plan|state|delivered [--ansi] [--store-root <dir>]
|
|
20
|
+
# report-screen archive <store_root> [--ansi]
|
|
21
|
+
#
|
|
22
|
+
# --ansi delegates to ScreenPaint (intent 317a, D1), the parser/re-layouter
|
|
23
|
+
# in the shared TUI core. Selection is by capability, never by harness:
|
|
24
|
+
# NO_COLOR forces plain, a non-TTY stdout stays plain unless
|
|
25
|
+
# PLASTIC_FORCE_COLOR=1 (the test seam), and an unparseable screen falls
|
|
26
|
+
# open to the plain text untouched.
|
|
27
|
+
#
|
|
28
|
+
# Exit codes:
|
|
29
|
+
# 0 - the screen is on stdout
|
|
30
|
+
# 2 - usage error, or the path/store is not what the verb expects (one line
|
|
31
|
+
# on stderr, stdout empty)
|
|
32
|
+
|
|
33
|
+
require "yaml"
|
|
34
|
+
require "shellwords"
|
|
35
|
+
require_relative "lib/report_screen"
|
|
36
|
+
require_relative "lib/intent_screen"
|
|
37
|
+
require_relative "lib/screen_paint"
|
|
38
|
+
require_relative "lib/session_ledger"
|
|
39
|
+
|
|
40
|
+
# Intent 331a (D6/R8): every caller-added screen kind file registers itself
|
|
41
|
+
# on load (ScreenPaint.register), so this glob is the ONLY wiring a new kind
|
|
42
|
+
# needs - no edit here, no edit to screen_paint.rb. Sorted for a
|
|
43
|
+
# deterministic load order; tolerates the directory being absent or empty
|
|
44
|
+
# (Dir.glob answers [] either way), which is this repo's own state today.
|
|
45
|
+
Dir.glob(File.join(__dir__, "lib", "screens", "*.rb")).sort.each { |f| require_relative f }
|
|
46
|
+
|
|
47
|
+
def usage_abort(message)
|
|
48
|
+
warn "report-screen: #{message}"
|
|
49
|
+
exit 2
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# The one place allowed to shell out (D2/D20): resolves the tag that CONTAINS
|
|
53
|
+
# the intent's merge commit, in the project's own repository, so the version
|
|
54
|
+
# on the delivered screen is the release that merge shipped in. The
|
|
55
|
+
# repository comes from --repo, else from projects.yml beside the store (the
|
|
56
|
+
# installed layout, <home>/projects/<slug>/store/<id>), else the repository
|
|
57
|
+
# this script lives in (the in-repo layout, `include_self: true`) - unless
|
|
58
|
+
# `include_self: false` (the branch reader, D23), in which case a global
|
|
59
|
+
# intent's repo never falls back to the script's own repository, since that
|
|
60
|
+
# is not the intent's repository at all. No merge sha, no repository, or no
|
|
61
|
+
# containing tag all answer nil, and the screen says "not recorded" rather
|
|
62
|
+
# than guessing from HEAD (D14). The pure module never reads git.
|
|
63
|
+
def resolve_repo(intent_dir, explicit_repo, script_dir, include_self: true)
|
|
64
|
+
candidates = []
|
|
65
|
+
candidates << File.expand_path(explicit_repo) if explicit_repo
|
|
66
|
+
if (m = intent_dir.match(%r{\A(.*)/projects/([^/]+)/store/[^/]+\z}))
|
|
67
|
+
home, slug = m[1], m[2]
|
|
68
|
+
projects_yml = File.join(home, "projects.yml")
|
|
69
|
+
if File.exist?(projects_yml)
|
|
70
|
+
data = begin
|
|
71
|
+
YAML.safe_load(File.read(projects_yml))
|
|
72
|
+
rescue StandardError
|
|
73
|
+
nil
|
|
74
|
+
end
|
|
75
|
+
entry = data.is_a?(Hash) && data["projects"].is_a?(Hash) ? data["projects"][slug] : nil
|
|
76
|
+
path = entry.is_a?(Hash) ? entry["path"].to_s : ""
|
|
77
|
+
candidates << File.expand_path(path) unless path.empty?
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
candidates << File.expand_path("..", script_dir) if include_self
|
|
81
|
+
candidates.find { |c| File.exist?(File.join(c, ".git")) }
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def git_tag_reader(explicit_repo:, script_dir:)
|
|
85
|
+
lambda do |intent_dir|
|
|
86
|
+
sha = ReportScreen.merge_sha(intent_dir)
|
|
87
|
+
return nil unless sha
|
|
88
|
+
repo = resolve_repo(intent_dir, explicit_repo, script_dir)
|
|
89
|
+
return nil unless repo
|
|
90
|
+
tags = `git -C #{repo.shellescape} tag --contains #{sha.shellescape} 2>/dev/null`
|
|
91
|
+
versions = tags.lines.map(&:strip).reject(&:empty?).filter_map do |tag|
|
|
92
|
+
v = tag.sub(/\Av/, "")
|
|
93
|
+
Gem::Version.correct?(v) ? [Gem::Version.new(v), tag] : nil
|
|
94
|
+
end
|
|
95
|
+
versions.min_by(&:first)&.last
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# D9/D23: the injected branch reader. flow_base (a pure YAML read, never
|
|
100
|
+
# git) wins when the intent's project names a flow base; otherwise the
|
|
101
|
+
# repository's remote HEAD, else whichever of main/master exists. Resolves
|
|
102
|
+
# the repo with `include_self: false` (D23): a global-store intent must
|
|
103
|
+
# never fall back to the script's OWN repository (~/.plastic itself, on
|
|
104
|
+
# "main" in the installed layout) and print a confident, wrong branch.
|
|
105
|
+
def git_branch_reader(explicit_repo:, script_dir:)
|
|
106
|
+
lambda do |intent_dir|
|
|
107
|
+
flow = ReportScreen.flow_base(intent_dir)
|
|
108
|
+
next flow if flow && !flow.to_s.empty?
|
|
109
|
+
|
|
110
|
+
repo = resolve_repo(intent_dir, explicit_repo, script_dir, include_self: false)
|
|
111
|
+
next nil unless repo
|
|
112
|
+
|
|
113
|
+
branch = `git -C #{repo.shellescape} symbolic-ref --short refs/remotes/origin/HEAD 2>/dev/null`.strip
|
|
114
|
+
branch = branch.sub(%r{\Aorigin/}, "")
|
|
115
|
+
next branch unless branch.empty?
|
|
116
|
+
|
|
117
|
+
%w[main master].find do |b|
|
|
118
|
+
system("git", "-C", repo, "rev-parse", "--verify", "--quiet", b, out: File::NULL, err: File::NULL)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
args = ARGV.dup
|
|
124
|
+
verb = args.shift
|
|
125
|
+
changed = nil
|
|
126
|
+
ansi = false
|
|
127
|
+
template_path = nil
|
|
128
|
+
repo_flag = nil
|
|
129
|
+
session_flag = nil
|
|
130
|
+
since_flag = nil
|
|
131
|
+
ledger_root_flag = nil
|
|
132
|
+
store_root_flag = nil
|
|
133
|
+
positional = []
|
|
134
|
+
|
|
135
|
+
while (arg = args.shift)
|
|
136
|
+
case arg
|
|
137
|
+
when "--changed"
|
|
138
|
+
usage_abort("--changed needs a value") if args.empty?
|
|
139
|
+
changed = args.shift
|
|
140
|
+
when "--ansi"
|
|
141
|
+
ansi = true
|
|
142
|
+
when "--all"
|
|
143
|
+
positional << "--all"
|
|
144
|
+
when "--repo"
|
|
145
|
+
repo_flag = args.shift or usage_abort("--repo needs a path")
|
|
146
|
+
when "--template"
|
|
147
|
+
template_path = args.shift or usage_abort("--template needs a path")
|
|
148
|
+
when "--session"
|
|
149
|
+
session_flag = args.shift or usage_abort("--session needs a value")
|
|
150
|
+
when "--since"
|
|
151
|
+
since_flag = args.shift or usage_abort("--since needs a value")
|
|
152
|
+
when "--ledger-root"
|
|
153
|
+
ledger_root_flag = args.shift or usage_abort("--ledger-root needs a path")
|
|
154
|
+
when "--store-root"
|
|
155
|
+
store_root_flag = args.shift or usage_abort("--store-root needs a path")
|
|
156
|
+
else
|
|
157
|
+
usage_abort("unknown flag #{arg.inspect}") if arg.start_with?("--") && arg != "--all"
|
|
158
|
+
positional << arg
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
usage_abort("usage: report-screen plan|state|delivered|delay|session <intent_dir> [--changed \"<text>\"] [--ansi]") unless verb
|
|
163
|
+
|
|
164
|
+
all_mode = positional.delete("--all") ? true : false
|
|
165
|
+
target = positional.first
|
|
166
|
+
|
|
167
|
+
ansi_enabled = ansi && ENV["NO_COLOR"].to_s.empty? &&
|
|
168
|
+
($stdout.tty? || ENV["PLASTIC_FORCE_COLOR"] == "1")
|
|
169
|
+
|
|
170
|
+
def paint(text, ansi_enabled, _renderer_path = nil)
|
|
171
|
+
return text unless ansi_enabled
|
|
172
|
+
ScreenPaint.paint(text, color: true) || text
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
case verb
|
|
176
|
+
when "plan"
|
|
177
|
+
usage_abort("usage: report-screen plan <intent_dir> [--ansi]") unless target
|
|
178
|
+
intent_dir = File.expand_path(target)
|
|
179
|
+
usage_abort("#{intent_dir} is not an intent directory") unless IntentScreen.intent_dir?(intent_dir)
|
|
180
|
+
store_root = File.expand_path("../..", intent_dir)
|
|
181
|
+
template_path ||= File.expand_path("../templates/report-plan.md", __dir__)
|
|
182
|
+
usage_abort("template not found at #{template_path}") unless File.exist?(template_path)
|
|
183
|
+
out = ReportScreen.render_plan(intent_dir: intent_dir, store_root: store_root,
|
|
184
|
+
template: File.read(template_path))
|
|
185
|
+
$stdout.write paint(out, ansi_enabled)
|
|
186
|
+
when "state"
|
|
187
|
+
if all_mode
|
|
188
|
+
usage_abort("usage: report-screen state --all <store_root>") unless target
|
|
189
|
+
store_root = File.expand_path(target)
|
|
190
|
+
usage_abort("#{store_root} is not a store (no INDEX.md)") unless File.exist?(File.join(store_root, "INDEX.md"))
|
|
191
|
+
out = ReportScreen.render_roster(store_root, changed: changed)
|
|
192
|
+
$stdout.write paint(out, ansi_enabled)
|
|
193
|
+
else
|
|
194
|
+
usage_abort("usage: report-screen state <intent_dir> [--changed \"<text>\"]") unless target
|
|
195
|
+
intent_dir = File.expand_path(target)
|
|
196
|
+
usage_abort("#{intent_dir} is not an intent directory") unless IntentScreen.intent_dir?(intent_dir)
|
|
197
|
+
store_root = File.expand_path("../..", intent_dir)
|
|
198
|
+
template_path ||= File.expand_path("../templates/report-state.md", __dir__)
|
|
199
|
+
usage_abort("template not found at #{template_path}") unless File.exist?(template_path)
|
|
200
|
+
out = ReportScreen.render_state(intent_dir: intent_dir, store_root: store_root, changed: changed,
|
|
201
|
+
template: File.read(template_path))
|
|
202
|
+
$stdout.write paint(out, ansi_enabled)
|
|
203
|
+
end
|
|
204
|
+
when "delivered"
|
|
205
|
+
usage_abort("usage: report-screen delivered <intent_dir>") unless target
|
|
206
|
+
intent_dir = File.expand_path(target)
|
|
207
|
+
usage_abort("#{intent_dir} is not an intent directory") unless IntentScreen.intent_dir?(intent_dir)
|
|
208
|
+
out = ReportScreen.render_delivered(intent_dir: intent_dir,
|
|
209
|
+
tag_reader: git_tag_reader(explicit_repo: repo_flag, script_dir: __dir__),
|
|
210
|
+
branch_reader: git_branch_reader(explicit_repo: repo_flag, script_dir: __dir__))
|
|
211
|
+
$stdout.write paint(out, ansi_enabled)
|
|
212
|
+
when "delay"
|
|
213
|
+
usage_abort("usage: report-screen delay <intent_dir>") unless target
|
|
214
|
+
intent_dir = File.expand_path(target)
|
|
215
|
+
usage_abort("#{intent_dir} is not an intent directory") unless IntentScreen.intent_dir?(intent_dir)
|
|
216
|
+
out = ReportScreen.render_delay(intent_dir: intent_dir)
|
|
217
|
+
$stdout.write paint(out, ansi_enabled)
|
|
218
|
+
when "session"
|
|
219
|
+
usage_abort("usage: report-screen session <tier_root> [--session <id>] [--since <iso>] [--ledger-root <dir>] [--ansi]") unless target
|
|
220
|
+
store_root = File.expand_path(target)
|
|
221
|
+
usage_abort("#{store_root} is not a store (no INDEX.md)") unless File.exist?(File.join(store_root, "INDEX.md"))
|
|
222
|
+
|
|
223
|
+
now = Time.now
|
|
224
|
+
session_id = session_flag || ENV[SessionLedger::SESSION_ID_ENV_KEY]
|
|
225
|
+
ledger_root = ledger_root_flag ? File.expand_path(ledger_root_flag) : ReportScreen.default_ledger_root(store_root)
|
|
226
|
+
|
|
227
|
+
if since_flag
|
|
228
|
+
begin
|
|
229
|
+
Time.parse(since_flag)
|
|
230
|
+
rescue ArgumentError
|
|
231
|
+
usage_abort("--since needs an ISO timestamp, got #{since_flag.inspect}")
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
dirs, skipped = ReportScreen.session_delivered_dirs(ledger_root: ledger_root, tier_root: store_root,
|
|
236
|
+
session: session_id, since: since_flag, now: now)
|
|
237
|
+
# D17: the widened window is announced whether the id was missing or simply
|
|
238
|
+
# matched nothing. A resumed background job carries a session id that is not
|
|
239
|
+
# the one on the ledger lines, and that case must not answer silently. An
|
|
240
|
+
# explicit --since is the caller naming the window, so it needs no note.
|
|
241
|
+
note =
|
|
242
|
+
if since_flag
|
|
243
|
+
nil
|
|
244
|
+
elsif session_id.nil? || session_id.to_s.strip.empty?
|
|
245
|
+
ReportScreen.window_note(ReportScreen.fallback_day(ledger_root, SessionLedger.day_id(now)),
|
|
246
|
+
"no session id given")
|
|
247
|
+
elsif !ReportScreen.session_tagged?(ledger_root: ledger_root, session: session_id, now: now)
|
|
248
|
+
ReportScreen.window_note(ReportScreen.fallback_day(ledger_root, SessionLedger.day_id(now)),
|
|
249
|
+
"this session has no line in the day ledger")
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
out = ReportScreen.render_session(
|
|
253
|
+
dirs: dirs, skipped: skipped, store_root: store_root,
|
|
254
|
+
tag_reader: git_tag_reader(explicit_repo: nil, script_dir: __dir__),
|
|
255
|
+
branch_reader: git_branch_reader(explicit_repo: nil, script_dir: __dir__),
|
|
256
|
+
note: note, now: now, painter: ->(text) { paint(text, ansi_enabled) }
|
|
257
|
+
)
|
|
258
|
+
$stdout.write out
|
|
259
|
+
when "roadmap"
|
|
260
|
+
usage_abort("usage: report-screen roadmap <roadmap.md> plan|state|delivered [--ansi] [--store-root <dir>]") if positional.empty?
|
|
261
|
+
roadmap_path = File.expand_path(positional[0])
|
|
262
|
+
usage_abort("#{roadmap_path} does not exist") unless File.exist?(roadmap_path)
|
|
263
|
+
sub_verb = positional[1]
|
|
264
|
+
usage_abort("usage: report-screen roadmap <roadmap.md> plan|state|delivered") unless sub_verb
|
|
265
|
+
unless %w[plan state delivered].include?(sub_verb)
|
|
266
|
+
usage_abort("unknown sub-verb #{sub_verb.inspect} (use plan|state|delivered)")
|
|
267
|
+
end
|
|
268
|
+
roadmap_store_root = store_root_flag ? File.expand_path(store_root_flag) : nil
|
|
269
|
+
out = ReportScreen.render_roadmap(path: roadmap_path, verb: sub_verb, store_root: roadmap_store_root)
|
|
270
|
+
$stdout.write paint(out, ansi_enabled)
|
|
271
|
+
when "archive"
|
|
272
|
+
usage_abort("usage: report-screen archive <store_root>") unless target
|
|
273
|
+
store_root = File.expand_path(target)
|
|
274
|
+
usage_abort("#{store_root} is not a store (no INDEX.md)") unless File.exist?(File.join(store_root, "INDEX.md"))
|
|
275
|
+
out = ReportScreen.render_archive(store_root)
|
|
276
|
+
$stdout.write paint(out, ansi_enabled)
|
|
277
|
+
else
|
|
278
|
+
usage_abort("unknown verb #{verb.inspect} (use plan|state|delivered|delay|session|roadmap|archive)")
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
exit 0
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# encoding: UTF-8
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
# roadmap-graph - the CLI over the roadmap graph model (intent 337, G4):
|
|
6
|
+
# `check`, `render`, `migrate`, every verb honouring --dry-run.
|
|
7
|
+
#
|
|
8
|
+
# Usage:
|
|
9
|
+
# roadmap-graph check <roadmap.md> [--dry-run]
|
|
10
|
+
# roadmap-graph render <roadmap.md> [--dry-run]
|
|
11
|
+
# roadmap-graph migrate <roadmap.md> [--dry-run]
|
|
12
|
+
#
|
|
13
|
+
# Exit codes:
|
|
14
|
+
# 0 - printed / rendered / migrated cleanly (or a migrate skip: the
|
|
15
|
+
# roadmap already carries a graph)
|
|
16
|
+
# 1 - a real finding: a cyclic graph, a dangling id, or a render/migrate
|
|
17
|
+
# refusal (no ## Graph, no grouping heading)
|
|
18
|
+
# 2 - usage: no argument, an unknown verb, or a missing/non-file path
|
|
19
|
+
|
|
20
|
+
require_relative "lib/roadmap_graph"
|
|
21
|
+
require_relative "lib/roadmap_render"
|
|
22
|
+
require_relative "lib/roadmap_migration"
|
|
23
|
+
|
|
24
|
+
module RoadmapGraphCli
|
|
25
|
+
module_function
|
|
26
|
+
|
|
27
|
+
VERBS = %w[check render migrate].freeze
|
|
28
|
+
|
|
29
|
+
def usage
|
|
30
|
+
warn "Usage: roadmap-graph <#{VERBS.join('|')}> <roadmap.md> [--dry-run]"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def main(argv)
|
|
34
|
+
args = argv.dup
|
|
35
|
+
dry_run = !!args.delete("--dry-run")
|
|
36
|
+
|
|
37
|
+
verb = args.shift
|
|
38
|
+
unless verb && VERBS.include?(verb)
|
|
39
|
+
warn "roadmap-graph: unknown verb #{verb.inspect} (use #{VERBS.join(', ')})" if verb
|
|
40
|
+
usage
|
|
41
|
+
return 2
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
path = args.shift
|
|
45
|
+
unless path && File.file?(path)
|
|
46
|
+
warn "roadmap-graph: #{path.inspect} is not a file"
|
|
47
|
+
return 2
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
case verb
|
|
51
|
+
when "check" then check(path)
|
|
52
|
+
when "render" then render(path, dry_run)
|
|
53
|
+
when "migrate" then migrate(path, dry_run)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def check(path)
|
|
58
|
+
result = RoadmapGraph.analyze(path)
|
|
59
|
+
unless result[:has_graph]
|
|
60
|
+
warn "roadmap-graph check: #{result[:reason]}"
|
|
61
|
+
return 1
|
|
62
|
+
end
|
|
63
|
+
if result[:cycle]
|
|
64
|
+
warn "roadmap-graph check: cyclic graph: #{result[:cycle].join(' > ')}"
|
|
65
|
+
return 1
|
|
66
|
+
end
|
|
67
|
+
if result[:reason]
|
|
68
|
+
warn "roadmap-graph check: #{result[:reason]}"
|
|
69
|
+
return 1
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
puts "Batches: #{result[:batches].map { |b| b.join(', ') }.join(' | ')}"
|
|
73
|
+
puts "Ready: #{result[:ready].join(', ')}"
|
|
74
|
+
puts "Dead ends: #{result[:dead_ends].join(', ')}" unless result[:dead_ends].empty?
|
|
75
|
+
|
|
76
|
+
unless result[:dangling].empty?
|
|
77
|
+
warn "roadmap-graph check: graph names #{result[:dangling].map(&:inspect).join(', ')}, no batch entry"
|
|
78
|
+
return 1
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
0
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def render(path, dry_run)
|
|
85
|
+
result = RoadmapRender.write(path, dry_run: dry_run)
|
|
86
|
+
unless result[:ok]
|
|
87
|
+
warn "roadmap-graph render: #{result[:error]}"
|
|
88
|
+
return 1
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
if dry_run
|
|
92
|
+
puts result[:content]
|
|
93
|
+
else
|
|
94
|
+
puts "roadmap-graph render: wrote #{path}"
|
|
95
|
+
end
|
|
96
|
+
0
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def migrate(path, dry_run)
|
|
100
|
+
result = RoadmapMigration.write(path, dry_run: dry_run)
|
|
101
|
+
unless result[:ok]
|
|
102
|
+
if result[:skipped]
|
|
103
|
+
puts "roadmap-graph migrate: #{result[:reason]}"
|
|
104
|
+
return 0
|
|
105
|
+
end
|
|
106
|
+
warn "roadmap-graph migrate: #{result[:reason]}"
|
|
107
|
+
return 1
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
if dry_run
|
|
111
|
+
puts result[:content]
|
|
112
|
+
else
|
|
113
|
+
puts "roadmap-graph migrate: wrote #{path}"
|
|
114
|
+
end
|
|
115
|
+
0
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
exit(RoadmapGraphCli.main(ARGV)) if $PROGRAM_NAME == __FILE__
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
# rebuilds an empty ledger.
|
|
19
19
|
|
|
20
20
|
require_relative "lib/roadmap_savepoint"
|
|
21
|
+
require_relative "lib/guarded_append"
|
|
21
22
|
|
|
22
23
|
def opt(args, name)
|
|
23
24
|
(i = args.index(name)) && args[i + 1]
|
|
@@ -47,6 +48,12 @@ when "append"
|
|
|
47
48
|
rescue ArgumentError => e
|
|
48
49
|
warn "append: #{e.message}"
|
|
49
50
|
exit 2
|
|
51
|
+
rescue GuardedAppend::Unavailable => e
|
|
52
|
+
# Intent 335 (spec D14): the write guard could not be taken; nothing was written. Exit 4
|
|
53
|
+
# rather than let a backtrace reach the six skill recipes that drive this CLI - it rescues
|
|
54
|
+
# ArgumentError alone today and would otherwise print one straight to the terminal.
|
|
55
|
+
warn "append: #{e.message}"
|
|
56
|
+
exit 4
|
|
50
57
|
end
|
|
51
58
|
|
|
52
59
|
when "rebuild"
|