@zalom/plastic 2.0.0-alpha.2 → 2.0.0-alpha.21
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/bin/test +24 -4
- package/hooks/call-budget +4 -0
- package/hooks/hooks.json +24 -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-call-budget +222 -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 +45 -7
- 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/compact_instructions.rb +5 -5
- 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 +102 -5
- package/scripts/lib/doctor_session_ledger.rb +52 -0
- package/scripts/lib/graph_edges.rb +137 -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 +28 -2
- package/scripts/lib/hook_replay.rb +211 -0
- package/scripts/lib/index_projection.rb +201 -0
- package/scripts/lib/installer_core.rb +141 -6
- 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/meter_watch.rb +179 -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 +908 -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 +506 -0
- package/scripts/lib/runner_policy.rb +173 -0
- package/scripts/lib/runner_proposals.rb +275 -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/session_usage.rb +190 -0
- package/scripts/lib/verify_intent.rb +33 -0
- package/scripts/lib/work_graph_validator.rb +201 -0
- package/scripts/meter-watch +57 -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/read-config +3 -3
- 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 +397 -0
- package/scripts/savepoint-note +69 -0
- package/scripts/session-usage +56 -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 +41 -34
- 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 +21 -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/config.yml +3 -3
- 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,74 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# encoding: UTF-8
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
# outcome-report - the CLI over scripts/lib/outcome_report.rb (intent 339, G6).
|
|
6
|
+
# Prints the generated outcome.md by default; --write puts it on disk through
|
|
7
|
+
# AtomicWrite. `end-intent` calls the library directly (never shells out to
|
|
8
|
+
# this), so this command is for a human or an agent checking the generated
|
|
9
|
+
# report before or instead of a close.
|
|
10
|
+
#
|
|
11
|
+
# Usage:
|
|
12
|
+
# outcome-report <intent_dir> [--write] [--disposition delivered|abandoned]
|
|
13
|
+
#
|
|
14
|
+
# Exit codes:
|
|
15
|
+
# 0 - printed (or written) successfully
|
|
16
|
+
# 2 - usage error: no path given, an unknown flag, or the path is not an
|
|
17
|
+
# intent directory
|
|
18
|
+
# 3 - the report model could not be built (a malformed or absent graph.md)
|
|
19
|
+
|
|
20
|
+
require_relative "lib/outcome_report"
|
|
21
|
+
require_relative "lib/intent_screen"
|
|
22
|
+
require_relative "lib/outcome_guard"
|
|
23
|
+
|
|
24
|
+
def usage_abort(message)
|
|
25
|
+
warn "outcome-report: #{message}"
|
|
26
|
+
exit 2
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
args = ARGV.dup
|
|
30
|
+
write_flag = false
|
|
31
|
+
disposition = "delivered"
|
|
32
|
+
positional = []
|
|
33
|
+
|
|
34
|
+
while (arg = args.shift)
|
|
35
|
+
case arg
|
|
36
|
+
when "--write"
|
|
37
|
+
write_flag = true
|
|
38
|
+
when "--disposition"
|
|
39
|
+
disposition = args.shift or usage_abort("--disposition needs a value")
|
|
40
|
+
else
|
|
41
|
+
usage_abort("unknown flag #{arg.inspect}") if arg.start_with?("--")
|
|
42
|
+
positional << arg
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
target = positional.first
|
|
47
|
+
usage_abort("usage: outcome-report <intent_dir> [--write] [--disposition delivered|abandoned]") unless target
|
|
48
|
+
# Row v1f.9 (N3): refuse an unknown --disposition here rather than write a
|
|
49
|
+
# record OutcomeGuard then refuses. OutcomeGuard::DISPOSITIONS is already
|
|
50
|
+
# loaded in-process, so this is one comparison, not a duplicated list.
|
|
51
|
+
unless OutcomeGuard::DISPOSITIONS.include?(disposition)
|
|
52
|
+
usage_abort("--disposition must be one of #{OutcomeGuard::DISPOSITIONS.join('|')}, got #{disposition.inspect}")
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
intent_dir = File.expand_path(target)
|
|
56
|
+
usage_abort("#{intent_dir} is not an intent directory") unless IntentScreen.intent_dir?(intent_dir)
|
|
57
|
+
|
|
58
|
+
model = OutcomeReport.model(intent_dir)
|
|
59
|
+
unless model[:ok]
|
|
60
|
+
warn "outcome-report: the report model could not be built: #{model[:errors].join('; ')}"
|
|
61
|
+
exit 3
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
if write_flag
|
|
65
|
+
OutcomeReport.write(intent_dir, disposition: disposition)
|
|
66
|
+
puts File.join(intent_dir, "outcome.md")
|
|
67
|
+
else
|
|
68
|
+
outcome_path = File.join(intent_dir, "outcome.md")
|
|
69
|
+
existing = File.exist?(outcome_path) ? File.read(outcome_path) : nil
|
|
70
|
+
puts OutcomeReport.render(model, disposition: disposition, existing: existing,
|
|
71
|
+
findings: OutcomeReport.findings(intent_dir), intent_dir: intent_dir)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
exit 0
|
package/scripts/plastic-lock
CHANGED
|
@@ -70,6 +70,7 @@ until ARGV.empty?
|
|
|
70
70
|
when "--model" then opts[:model] = ARGV.shift
|
|
71
71
|
when "--thread" then opts[:thread] = ARGV.shift
|
|
72
72
|
when "--mode" then opts[:mode] = ARGV.shift
|
|
73
|
+
when "--allow-inline" then opts[:allow_inline] = true
|
|
73
74
|
when "--status" then opts[:status] = ARGV.shift
|
|
74
75
|
else
|
|
75
76
|
warn "unknown flag #{flag}"
|
|
@@ -154,7 +155,8 @@ end
|
|
|
154
155
|
case verb
|
|
155
156
|
when "arm"
|
|
156
157
|
result = Arm.arm(intent_dir: dir, session: key, mode: opts[:mode] || "auto", home: home,
|
|
157
|
-
harness: harness, agent: opts[:agent], model: opts[:model], thread: opts[:thread]
|
|
158
|
+
harness: harness, agent: opts[:agent], model: opts[:model], thread: opts[:thread],
|
|
159
|
+
allow_inline: opts[:allow_inline] || false)
|
|
158
160
|
case result[:status]
|
|
159
161
|
when :acquired, :owned
|
|
160
162
|
wt = result[:worktree] || {}
|
|
@@ -178,6 +180,11 @@ when "arm"
|
|
|
178
180
|
when :corrupt
|
|
179
181
|
warn "plastic-lock: delivery.lock for intent #{intent_id} is unreadable; #{doctor_hint('fix the lock', hint_harness)}"
|
|
180
182
|
exit 1
|
|
183
|
+
when :inline_refused
|
|
184
|
+
warn "plastic-lock: refusing to arm intent #{intent_id} from a conversation session " \
|
|
185
|
+
"(owner rule 2026-08-31: no inline intent delivery). Dispatch the delivery team " \
|
|
186
|
+
"instead (plastic-auto), or pass --allow-inline with explicit owner approval."
|
|
187
|
+
exit 1
|
|
181
188
|
end
|
|
182
189
|
when "status"
|
|
183
190
|
lock = Lock.read(dir)
|
package/scripts/read-config
CHANGED
|
@@ -14,9 +14,9 @@ require_relative "lib/agent_models"
|
|
|
14
14
|
DEFAULTS = {
|
|
15
15
|
"version" => 3,
|
|
16
16
|
"stale_threshold_days" => 3,
|
|
17
|
-
# Absolute token counts for a 1M window,
|
|
18
|
-
"context_offer_tokens" =>
|
|
19
|
-
"context_insist_tokens" =>
|
|
17
|
+
# Absolute token counts for a 1M window, 15 and 25 percent (intent 355, n5, D7).
|
|
18
|
+
"context_offer_tokens" => 150_000,
|
|
19
|
+
"context_insist_tokens" => 250_000,
|
|
20
20
|
"execution_mode" => "subagent-driven",
|
|
21
21
|
"hash_length" => 6,
|
|
22
22
|
"hash_algorithm" => "sha256-base36",
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# encoding: UTF-8
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
# ready-set - the CLI over ReadySet (intent 336, G3). No computed field this
|
|
6
|
+
# intent adds ships without a script that reads it (327 D18, C19): prints the
|
|
7
|
+
# ready order, the blockers for every unready node, the batches, and every
|
|
8
|
+
# maximal-length critical path; emits JSON for a caller like the runner (G7).
|
|
9
|
+
#
|
|
10
|
+
# Usage:
|
|
11
|
+
# ready-set <intent_dir> [--json] [--ranker finish-first|critical-path]
|
|
12
|
+
#
|
|
13
|
+
# Exit codes (matching scripts/validate-work-graph's shape):
|
|
14
|
+
# 0 - printed (or emitted as JSON)
|
|
15
|
+
# 1 - the graph is invalid (missing, cyclic, or a malformed node file)
|
|
16
|
+
# 2 - usage: not an intent directory, or an unknown --ranker value
|
|
17
|
+
|
|
18
|
+
require "json"
|
|
19
|
+
require_relative "lib/ready_set"
|
|
20
|
+
require_relative "lib/savepoint"
|
|
21
|
+
|
|
22
|
+
module ReadySetCli
|
|
23
|
+
module_function
|
|
24
|
+
|
|
25
|
+
RANKERS = {
|
|
26
|
+
"finish-first" => -> { ReadySet::FinishFirstRanker.new },
|
|
27
|
+
"critical-path" => -> { ReadySet::CriticalPathRanker.new },
|
|
28
|
+
}.freeze
|
|
29
|
+
|
|
30
|
+
def usage
|
|
31
|
+
warn "Usage: ready-set <intent_dir> [--json] [--ranker #{RANKERS.keys.join('|')}]"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def intent_directory?(dir)
|
|
35
|
+
dir && File.directory?(dir) && File.exist?(Savepoint.intent_file(dir))
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def main(argv)
|
|
39
|
+
args = argv.dup
|
|
40
|
+
json = !!args.delete("--json")
|
|
41
|
+
|
|
42
|
+
ranker_name = nil
|
|
43
|
+
if (i = args.index("--ranker"))
|
|
44
|
+
ranker_name = args[i + 1]
|
|
45
|
+
args.slice!(i, 2)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
intent_dir_arg = args.shift
|
|
49
|
+
unless intent_directory?(intent_dir_arg && File.expand_path(intent_dir_arg))
|
|
50
|
+
warn "ready-set: #{intent_dir_arg.inspect} is not an intent directory"
|
|
51
|
+
usage
|
|
52
|
+
return 2
|
|
53
|
+
end
|
|
54
|
+
intent_dir = File.expand_path(intent_dir_arg)
|
|
55
|
+
|
|
56
|
+
ranker = nil
|
|
57
|
+
if ranker_name
|
|
58
|
+
factory = RANKERS[ranker_name]
|
|
59
|
+
unless factory
|
|
60
|
+
warn "ready-set: unknown ranker #{ranker_name.inspect} (use #{RANKERS.keys.join(', ')})"
|
|
61
|
+
return 2
|
|
62
|
+
end
|
|
63
|
+
ranker = factory.call
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
analysis = ReadySet.analyze(intent_dir, ranker: ranker)
|
|
67
|
+
unless analysis[:ok]
|
|
68
|
+
warn "ready-set: #{analysis[:errors].join('; ')}"
|
|
69
|
+
return 1
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
if json
|
|
73
|
+
puts JSON.pretty_generate(json_payload(analysis))
|
|
74
|
+
else
|
|
75
|
+
print_report(analysis)
|
|
76
|
+
end
|
|
77
|
+
0
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def json_payload(analysis)
|
|
81
|
+
unready = analysis[:nodes].reject { |_, v| v[:ready] }
|
|
82
|
+
{
|
|
83
|
+
"ready" => analysis[:ranked_ready].map { |r| r[:id] },
|
|
84
|
+
"ranker" => analysis[:ranker_name],
|
|
85
|
+
"batches" => analysis[:batches],
|
|
86
|
+
"critical_path" => analysis[:critical_path],
|
|
87
|
+
"paths" => analysis[:paths],
|
|
88
|
+
"hops" => analysis[:hops],
|
|
89
|
+
"max_paths_total" => analysis[:max_paths_total],
|
|
90
|
+
"blockers" => unready.transform_values { |v| v[:blockers] },
|
|
91
|
+
"errors" => analysis[:errors],
|
|
92
|
+
}
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def print_report(analysis)
|
|
96
|
+
unready = analysis[:nodes].reject { |_, v| v[:ready] }
|
|
97
|
+
|
|
98
|
+
puts "Ranker: #{analysis[:ranker_name]}"
|
|
99
|
+
puts
|
|
100
|
+
puts "Ready (#{analysis[:ranked_ready].length}):"
|
|
101
|
+
if analysis[:ranked_ready].empty?
|
|
102
|
+
puts " (none)"
|
|
103
|
+
else
|
|
104
|
+
analysis[:ranked_ready].each { |r| puts " #{r[:id]}" }
|
|
105
|
+
end
|
|
106
|
+
puts
|
|
107
|
+
puts "Blockers:"
|
|
108
|
+
if unready.empty?
|
|
109
|
+
puts " (none)"
|
|
110
|
+
else
|
|
111
|
+
unready.each do |id, view|
|
|
112
|
+
puts " #{id}:"
|
|
113
|
+
view[:blockers].each { |b| puts " - #{b}" }
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
puts
|
|
117
|
+
puts "Batches:"
|
|
118
|
+
analysis[:batches].each_with_index { |batch, i| puts " #{i + 1}: #{batch.join(', ')}" }
|
|
119
|
+
puts
|
|
120
|
+
puts "Critical path (#{analysis[:hops]} hops): #{(analysis[:critical_path] || []).join(' > ')}"
|
|
121
|
+
puts "Every maximal-length path (#{analysis[:max_paths_total]} total, #{analysis[:paths].length} shown):"
|
|
122
|
+
analysis[:paths].each { |p| puts " #{p.join(' > ')}" }
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
exit(ReadySetCli.main(ARGV)) if $PROGRAM_NAME == __FILE__
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# encoding: UTF-8
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
# release-check - deterministic CLI over ReleaseGuard (intent 347).
|
|
6
|
+
#
|
|
7
|
+
# Guards a tagged publish before the workflow ever reaches `npm publish`:
|
|
8
|
+
# the pushed tag must equal `v` plus the version in package.json, the three
|
|
9
|
+
# repo version files must agree, and the runner's npm must meet the 11.5.1
|
|
10
|
+
# floor npm requires for OIDC trusted publishing. Writes the derived
|
|
11
|
+
# dist-tag and version to $GITHUB_OUTPUT, so the alpha/beta/latest rule has
|
|
12
|
+
# exactly one implementation, ReleaseGuard.dist_tag, shared with the test
|
|
13
|
+
# suite.
|
|
14
|
+
#
|
|
15
|
+
# Usage:
|
|
16
|
+
# release-check --tag <tag> --npm-version <version>
|
|
17
|
+
# [--github-output <path>] [--root <dir>]
|
|
18
|
+
#
|
|
19
|
+
# Exit codes: 0 (clean), 1 (violations; reported on stderr, one per line,
|
|
20
|
+
# each naming the specific thing that is wrong), 2 (usage).
|
|
21
|
+
|
|
22
|
+
require "json"
|
|
23
|
+
|
|
24
|
+
require_relative "lib/release_guard"
|
|
25
|
+
|
|
26
|
+
NPM_FLOOR = [11, 5, 1].freeze
|
|
27
|
+
KNOWN_FLAGS = %w[--tag --npm-version --github-output --root].freeze
|
|
28
|
+
|
|
29
|
+
def usage
|
|
30
|
+
warn "usage: release-check --tag <tag> --npm-version <version> [--github-output <path>] [--root <dir>]"
|
|
31
|
+
exit 2
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def parse_args(argv)
|
|
35
|
+
opts = {}
|
|
36
|
+
i = 0
|
|
37
|
+
while i < argv.length
|
|
38
|
+
flag = argv[i]
|
|
39
|
+
usage unless KNOWN_FLAGS.include?(flag)
|
|
40
|
+
|
|
41
|
+
value = argv[i + 1]
|
|
42
|
+
usage if value.nil?
|
|
43
|
+
|
|
44
|
+
opts[flag] = value
|
|
45
|
+
i += 2
|
|
46
|
+
end
|
|
47
|
+
opts
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def read_version(path)
|
|
51
|
+
JSON.parse(File.read(path))["version"]
|
|
52
|
+
rescue Errno::ENOENT, JSON::ParserError
|
|
53
|
+
nil
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
opts = parse_args(ARGV)
|
|
57
|
+
usage unless opts["--tag"] && opts["--npm-version"]
|
|
58
|
+
|
|
59
|
+
root = opts["--root"] ? File.expand_path(opts["--root"]) : File.expand_path("..", __dir__)
|
|
60
|
+
package_json_path = File.join(root, "package.json")
|
|
61
|
+
plugin_json_path = File.join(root, ".claude-plugin", "plugin.json")
|
|
62
|
+
marketplace_json_path = File.join(root, ".claude-plugin", "marketplace.json")
|
|
63
|
+
|
|
64
|
+
violations = []
|
|
65
|
+
|
|
66
|
+
canonical_version = read_version(package_json_path)
|
|
67
|
+
violations << "could not read a version from #{package_json_path}" if canonical_version.nil?
|
|
68
|
+
|
|
69
|
+
tag = opts["--tag"].sub(%r{\Arefs/tags/}, "")
|
|
70
|
+
dist_tag = nil
|
|
71
|
+
|
|
72
|
+
if canonical_version
|
|
73
|
+
expected_tag = "v#{canonical_version}"
|
|
74
|
+
if tag != expected_tag
|
|
75
|
+
violations << "tag #{tag} does not match package.json version #{canonical_version} (expected #{expected_tag})"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
dist_tag = ReleaseGuard.dist_tag(canonical_version)
|
|
79
|
+
stable = dist_tag == "latest"
|
|
80
|
+
|
|
81
|
+
result = ReleaseGuard.check(
|
|
82
|
+
package_json: package_json_path,
|
|
83
|
+
plugin_json: plugin_json_path,
|
|
84
|
+
marketplace_json: marketplace_json_path,
|
|
85
|
+
stable: stable
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
unless result.ok?
|
|
89
|
+
result.mismatches.each do |file|
|
|
90
|
+
violations << "version file #{file} does not agree with package.json (#{canonical_version})"
|
|
91
|
+
end
|
|
92
|
+
if stable && result.prerelease_suffix
|
|
93
|
+
violations << "stable cut #{canonical_version} carries the pre-release suffix -#{result.prerelease_suffix}"
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
npm_version = opts["--npm-version"].to_s.strip
|
|
99
|
+
if npm_version.empty? || !npm_version.match?(/\A\d+(\.\d+)*\z/)
|
|
100
|
+
violations << "npm --version reported #{opts["--npm-version"].inspect}, which is empty or not numeric"
|
|
101
|
+
elsif (npm_version.split(".").map(&:to_i) <=> NPM_FLOOR) < 0
|
|
102
|
+
violations << "npm #{npm_version} is below the 11.5.1 floor OIDC trusted publishing requires"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
if violations.empty?
|
|
106
|
+
if opts["--github-output"]
|
|
107
|
+
File.open(opts["--github-output"], "a") do |f|
|
|
108
|
+
f.puts "version=#{canonical_version}"
|
|
109
|
+
f.puts "dist_tag=#{dist_tag}"
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
puts "OK: tag #{tag}, version #{canonical_version}, dist-tag #{dist_tag}"
|
|
113
|
+
exit 0
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
warn "VIOLATIONS:"
|
|
117
|
+
violations.each { |v| warn "- #{v}" }
|
|
118
|
+
exit 1
|
|
@@ -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
|