@zalom/plastic 2.0.1 → 2.0.3
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/PLASTIC.md +2 -1
- package/README.md +40 -33
- package/agents/plastic-enforcer.md +13 -11
- package/agents/plastic-executor.md +2 -1
- package/agents/plastic-primary-advisor.md +2 -2
- package/agents/plastic-secondary-advisor.md +2 -2
- package/bin/plastic +2 -0
- package/docs/help/agent-architecture.md +9 -8
- package/docs/help/agent-report-contract.md +6 -4
- package/docs/help/completion-and-done.md +20 -4
- package/docs/help/human-report-contract.md +23 -20
- package/docs/help/knowledge-graph.md +2 -2
- package/docs/help/lifecycle-and-savepoints.md +9 -5
- package/docs/help/locks-and-worktrees.md +4 -3
- package/docs/help/maintenance-and-revisions.md +2 -2
- package/docs/help/roadmaps.md +16 -11
- package/docs/help/track-1-guided.md +48 -30
- package/docs/help/track-2-auto.md +35 -10
- package/docs/help/track-3-projects-and-roadmaps.md +10 -7
- package/docs/help/tutorial.md +421 -0
- package/package.json +1 -1
- package/scripts/end-intent +312 -69
- package/scripts/lib/arm.rb +28 -10
- package/scripts/lib/cli/commands/auto_lock.rb +17 -5
- package/scripts/lib/cli/commands/auto_take.rb +38 -1
- package/scripts/lib/cli/commands/doctor.rb +2 -2
- package/scripts/lib/cli/commands/intent_command.rb +6 -1
- package/scripts/lib/cli/commands/intent_end.rb +15 -2
- package/scripts/lib/cli/commands/intent_step.rb +8 -0
- package/scripts/lib/cli/commands/project_links.rb +5 -1
- package/scripts/lib/cli/commands/project_new.rb +24 -1
- package/scripts/lib/cli/commands/render.rb +3 -1
- package/scripts/lib/cli/commands/roadmap_next.rb +10 -3
- package/scripts/lib/cli/commands/roadmap_show.rb +21 -1
- package/scripts/lib/cli/commands/session_commit.rb +1 -1
- package/scripts/lib/cli/commands/sync.rb +10 -1
- package/scripts/lib/hook_replay.rb +51 -45
- package/scripts/lib/installer_core.rb +12 -0
- package/scripts/lib/node_input.rb +13 -5
- package/scripts/lib/revisions_writer.rb +1 -2
- package/scripts/lib/roadmap_queue.rb +12 -4
- package/scripts/lib/runner_dispatch.rb +8 -7
- package/scripts/lib/session_git.rb +42 -1
- package/scripts/lib/untouched_scaffold.rb +51 -0
- package/scripts/plastic-lock +24 -28
- package/scripts/project-links +6 -21
- package/scripts/roadmap-graph +10 -4
- package/scripts/rollback.rb +5 -1
- package/scripts/update.rb +7 -1
- package/skills/_decision-tables.md +3 -3
|
@@ -46,9 +46,9 @@ module RunnerDispatch
|
|
|
46
46
|
|
|
47
47
|
HARD_CAP_RE = /\Ais at its dispatch cap \((\d+)\/(\d+)\)\z/.freeze
|
|
48
48
|
|
|
49
|
-
#
|
|
50
|
-
#
|
|
51
|
-
#
|
|
49
|
+
# The spawn block's default agent. A dispatched node names its own kind's
|
|
50
|
+
# node agent instead (HarnessAdapter.agent_type_for_kind), the same agent
|
|
51
|
+
# the dispatch line under it names (acceptance N12).
|
|
52
52
|
SPAWN_AGENT = "plastic-executor"
|
|
53
53
|
|
|
54
54
|
# matrix 6.1/6.2: one spawn block per dispatched node - agent, the model
|
|
@@ -237,7 +237,8 @@ module RunnerDispatch
|
|
|
237
237
|
provisioned = RunnerPolicy.worktree?(kind) ? worktree.provision(context, node: node, kind: kind, runner: runner)
|
|
238
238
|
: unprovisioned
|
|
239
239
|
node_reader = lambda do |intent_dir:|
|
|
240
|
-
{ "code" => provisioned[:path], "code_branch" => provisioned[:branch], "provisioned" => !!provisioned[:provisioned]
|
|
240
|
+
{ "code" => provisioned[:path], "code_branch" => provisioned[:branch], "provisioned" => !!provisioned[:provisioned],
|
|
241
|
+
"read_only" => !RunnerPolicy.worktree?(kind) }
|
|
241
242
|
end
|
|
242
243
|
|
|
243
244
|
# Row 5.18: build the node input BEFORE writing `running` - a `running` line
|
|
@@ -289,7 +290,7 @@ module RunnerDispatch
|
|
|
289
290
|
|
|
290
291
|
test_command = NodeInput.test_command_block(intent_dir: intent_dir, files: (nodes_decl[node] || {})[:files])
|
|
291
292
|
spawn = spawn_block(model: model, effort: effort, input: build_result[:path], test_command: test_command,
|
|
292
|
-
call_cap: calls_cap)
|
|
293
|
+
call_cap: calls_cap, agent: HarnessAdapter.agent_type_for_kind(kind))
|
|
293
294
|
|
|
294
295
|
{
|
|
295
296
|
ok: true,
|
|
@@ -409,9 +410,9 @@ module RunnerDispatch
|
|
|
409
410
|
end
|
|
410
411
|
|
|
411
412
|
# Row 5.32: re-arms delivery.lock for a resumed session with a new id -
|
|
412
|
-
# `plastic
|
|
413
|
+
# `plastic auto take` is the public command that takes ownership again.
|
|
413
414
|
def rearm_command(intent_dir)
|
|
414
|
-
"plastic
|
|
415
|
+
"plastic auto take #{File.basename(intent_dir.to_s).split("--").first}"
|
|
415
416
|
end
|
|
416
417
|
|
|
417
418
|
# --- refusals and the report -------------------------------------------------
|
|
@@ -7,6 +7,7 @@ require_relative "worktree"
|
|
|
7
7
|
require_relative "scaffold_intent"
|
|
8
8
|
require_relative "session_ledger"
|
|
9
9
|
require_relative "active_delivery"
|
|
10
|
+
require_relative "lock"
|
|
10
11
|
|
|
11
12
|
# SessionGit - the branch model and per-repo flow settings behind
|
|
12
13
|
# `scripts/session-commit` (intent 300), which is how a checklist item
|
|
@@ -306,6 +307,44 @@ module SessionGit
|
|
|
306
307
|
parts.each_cons(2).any? { |a, b| a == ".claude" && b == "worktrees" }
|
|
307
308
|
end
|
|
308
309
|
|
|
310
|
+
# Why an agent-owned branch was left alone, named from the real delivery
|
|
311
|
+
# lock (acceptance N5). The branch or worktree name gives the intent id; the
|
|
312
|
+
# repository's slug in projects.yml gives its store. A branch whose intent
|
|
313
|
+
# cannot be found is still left alone, and says so plainly.
|
|
314
|
+
def delivery_state(repo, branch, plastic_home, session)
|
|
315
|
+
intent_dir = delivery_intent_dir(repo, branch, plastic_home)
|
|
316
|
+
return "it is an intent delivery branch" if intent_dir.nil?
|
|
317
|
+
|
|
318
|
+
id = File.basename(intent_dir)[/\A\d+/]
|
|
319
|
+
owner = Lock.read(intent_dir).to_h["owner_session"].to_s
|
|
320
|
+
return "it is intent #{id}'s delivery branch and no delivery lock is held" if owner.empty?
|
|
321
|
+
if !blank?(session) && owner.start_with?(session.to_s)
|
|
322
|
+
return "this session holds intent #{id}'s delivery lock; commit delivery work there with git"
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
"intent #{id}'s delivery lock is held by #{owner}"
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
def delivery_intent_dir(repo, branch, plastic_home)
|
|
329
|
+
parts = File.expand_path(repo.to_s).split(File::SEPARATOR)
|
|
330
|
+
at = parts.each_cons(2).find_index { |a, b| a == ".claude" && b == "worktrees" }
|
|
331
|
+
name = branch.to_s.start_with?("plastic/") ? branch.to_s.delete_prefix("plastic/") : parts.last
|
|
332
|
+
main = at ? parts[0...at].join(File::SEPARATOR) : File.expand_path(repo.to_s)
|
|
333
|
+
id = name[/\A(\d+)--/, 1]
|
|
334
|
+
slug = slug_for_repo(main, plastic_home)
|
|
335
|
+
return nil if id.nil? || slug.nil?
|
|
336
|
+
|
|
337
|
+
Dir.glob(File.join(Plastic::StoreLayout.root(plastic_home, slug), "store", "#{id}--*")).first
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def slug_for_repo(main, plastic_home)
|
|
341
|
+
projects = YAML.safe_load_file(File.join(plastic_home, "projects.yml")).fetch("projects")
|
|
342
|
+
target = File.realpath(main)
|
|
343
|
+
projects.find { |_slug, info| File.exist?(info["path"].to_s) && File.realpath(info["path"]) == target }&.first
|
|
344
|
+
rescue StandardError
|
|
345
|
+
nil
|
|
346
|
+
end
|
|
347
|
+
|
|
309
348
|
def stage_and_commit(dir, subject, runner:, body: nil)
|
|
310
349
|
runner.run("-C", dir, "add", "-A")
|
|
311
350
|
args = ["-C", dir, "commit", "-m", subject]
|
|
@@ -340,7 +379,9 @@ module SessionGit
|
|
|
340
379
|
|
|
341
380
|
branch_now = current_branch(repo, runner: runner)
|
|
342
381
|
return note("detached HEAD: no commit") if branch_now == "HEAD"
|
|
343
|
-
|
|
382
|
+
if agent_owned?(repo, branch_now)
|
|
383
|
+
return note("left branch #{branch_now} untouched: #{delivery_state(repo, branch_now, plastic_home, session)}")
|
|
384
|
+
end
|
|
344
385
|
return note("repository has no commits yet: no commit") unless has_commits?(repo, runner: runner)
|
|
345
386
|
|
|
346
387
|
flow, flow_notes = load_flow(cwd: cwd, repo: repo, plastic_home: plastic_home, runner: runner)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "savepoint"
|
|
4
|
+
require_relative "backfill_intent"
|
|
5
|
+
|
|
6
|
+
# Finds an intent that is still exactly what new-intent created, so
|
|
7
|
+
# end-intent can refuse to close it as delivered.
|
|
8
|
+
#
|
|
9
|
+
# The rule is narrow so older intents still close. An intent is untouched
|
|
10
|
+
# only when all of these hold:
|
|
11
|
+
#
|
|
12
|
+
# - spec.md, plan.md, checklist.md and outcome.md exist and are still
|
|
13
|
+
# unedited placeholders, with no ticked checklist item;
|
|
14
|
+
# - there is no action, node or graph file;
|
|
15
|
+
# - savepoint.md holds only What lines;
|
|
16
|
+
# - the code worktree, if there is one, has no changes.
|
|
17
|
+
#
|
|
18
|
+
# An older intent with missing files is never untouched.
|
|
19
|
+
module UntouchedScaffold
|
|
20
|
+
module_function
|
|
21
|
+
|
|
22
|
+
LIFECYCLE = %w[spec.md plan.md checklist.md outcome.md].freeze
|
|
23
|
+
SCAFFOLD_LINE = /\A\S+\s+What\s/
|
|
24
|
+
TICKED_ITEM = /^\s*- \[[xX]\] /
|
|
25
|
+
|
|
26
|
+
def reason(intent_dir, templates: {}, worktree_changed: ->(_dir) { false })
|
|
27
|
+
return nil unless placeholders_untouched?(intent_dir, templates)
|
|
28
|
+
return nil if Savepoint.has_real_action?(intent_dir)
|
|
29
|
+
return nil if File.exist?(File.join(intent_dir, "graph.md"))
|
|
30
|
+
return nil unless scaffold_savepoint?(File.join(intent_dir, "savepoint.md"))
|
|
31
|
+
return nil if worktree_changed.call(intent_dir)
|
|
32
|
+
|
|
33
|
+
"spec.md, plan.md, checklist.md and outcome.md are still placeholders, " \
|
|
34
|
+
"no action, node or graph was written, the savepoint records no work, " \
|
|
35
|
+
"and the code worktree has no changes"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def placeholders_untouched?(intent_dir, templates)
|
|
39
|
+
paths = LIFECYCLE.map { |rel| File.join(intent_dir, rel) }
|
|
40
|
+
return false unless paths.all? { |path| File.exist?(path) && !Savepoint.stage_file_present?(path) }
|
|
41
|
+
return false if File.read(File.join(intent_dir, "checklist.md")).match?(TICKED_ITEM)
|
|
42
|
+
|
|
43
|
+
BackfillIntent.classify(intent_dir, templates: templates)[:edited].empty?
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def scaffold_savepoint?(path)
|
|
47
|
+
return false unless File.exist?(path)
|
|
48
|
+
|
|
49
|
+
File.readlines(path).reject { |line| line.strip.empty? }.all? { |line| line.match?(SCAFFOLD_LINE) }
|
|
50
|
+
end
|
|
51
|
+
end
|
package/scripts/plastic-lock
CHANGED
|
@@ -15,8 +15,9 @@
|
|
|
15
15
|
# arm take the intent for this session (intent 307): acquire
|
|
16
16
|
# delivery.lock stamped with --mode (default auto) and the
|
|
17
17
|
# provenance flags, provision the code worktree, and point the
|
|
18
|
-
# session at the intent; exit
|
|
19
|
-
# corrupt
|
|
18
|
+
# session at the intent; exit 3 on a held, stale, or excluded
|
|
19
|
+
# lock (another owner's), exit 1 on a corrupt one, each with the
|
|
20
|
+
# public command that resolves it
|
|
20
21
|
# who compact, read-only durable owner/activity view
|
|
21
22
|
# status report the lock file, its freshness, the derived worktree,
|
|
22
23
|
# whether this session is the one delivering the intent, and
|
|
@@ -36,7 +37,8 @@
|
|
|
36
37
|
# configured project roots): exactly one fresh lock this session owns or
|
|
37
38
|
# delegates for. Session resolution no longer reads any per-session state
|
|
38
39
|
# file; that path was retired in 2.0 (intent 307) and 344 G11.
|
|
39
|
-
# Exit 0 on success/report; exit
|
|
40
|
+
# Exit 0 on success/report; exit 3 when another session owns the lock (arm,
|
|
41
|
+
# fix, release, reclaim); exit 1 when the verb is otherwise blocked.
|
|
40
42
|
|
|
41
43
|
require_relative "lib/store_layout"
|
|
42
44
|
require "json"
|
|
@@ -82,7 +84,6 @@ usage! if opts[:mode] && !%w[auto guided].include?(opts[:mode])
|
|
|
82
84
|
session = opts[:session]
|
|
83
85
|
session = ENV["CLAUDE_CODE_SESSION_ID"] if session.nil? || session.strip.empty?
|
|
84
86
|
harness = opts[:harness]
|
|
85
|
-
hint_harness = harness || "claude"
|
|
86
87
|
# The plastic home every hook uses (PLASTIC_HOME, else ~/.plastic); Arm takes the
|
|
87
88
|
# OS home above it, the same convention Worktree resolves projects.yml from.
|
|
88
89
|
plastic_home_env = ENV["PLASTIC_HOME"].to_s.strip
|
|
@@ -149,8 +150,11 @@ intent_id = Arm.intent_id_for(dir)
|
|
|
149
150
|
store = File.dirname(dir)
|
|
150
151
|
key = Arm.resolve_session(session, store: store, intent_id: intent_id)
|
|
151
152
|
|
|
152
|
-
|
|
153
|
-
|
|
153
|
+
# Exit 3 is Plastic's refusal: the lock belongs to another session, and only
|
|
154
|
+
# its owner (or the person who owns the work) may resolve it.
|
|
155
|
+
def refuse(message)
|
|
156
|
+
warn "plastic-lock: #{message}"
|
|
157
|
+
exit 3
|
|
154
158
|
end
|
|
155
159
|
|
|
156
160
|
case verb
|
|
@@ -167,19 +171,16 @@ when "arm"
|
|
|
167
171
|
"worktree" => wt
|
|
168
172
|
)
|
|
169
173
|
when :held
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
exit 1
|
|
174
|
+
refuse "delivery lock for intent #{intent_id} is held by session " \
|
|
175
|
+
"#{result[:lock] && result[:lock]['owner_session']}; plastic auto lock status #{intent_id} shows it"
|
|
173
176
|
when :stale
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
exit 1
|
|
177
|
+
refuse "delivery lock for intent #{intent_id} is stale (owner " \
|
|
178
|
+
"#{result[:lock] && result[:lock]['owner_session']}); #{Arm.stale_hint(intent_id)}"
|
|
177
179
|
when :excluded
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
exit 1
|
|
180
|
+
refuse "a #{result[:lock] && result[:lock]['type']} lock is active on intent " \
|
|
181
|
+
"#{intent_id}; plastic auto lock status #{intent_id} shows it"
|
|
181
182
|
when :corrupt
|
|
182
|
-
warn "plastic-lock: delivery.lock for intent #{intent_id} is unreadable;
|
|
183
|
+
warn "plastic-lock: delivery.lock for intent #{intent_id} is unreadable; plastic auto lock fix #{intent_id} rewrites it"
|
|
183
184
|
exit 1
|
|
184
185
|
when :inline_refused
|
|
185
186
|
warn "plastic-lock: refusing to arm intent #{intent_id} from a conversation session " \
|
|
@@ -207,32 +208,27 @@ when "status"
|
|
|
207
208
|
when "fix"
|
|
208
209
|
report = Arm.repair(intent_dir: dir, session: key, home: home, harness: harness,
|
|
209
210
|
agent: opts[:agent], model: opts[:model], thread: opts[:thread],
|
|
210
|
-
run_mode: opts[:mode]
|
|
211
|
+
run_mode: opts[:mode])
|
|
211
212
|
puts JSON.pretty_generate(report)
|
|
212
213
|
unless report["status"] == "repaired"
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
exit 1
|
|
214
|
+
refuse "#{report['status']} by #{report['owner']}" \
|
|
215
|
+
"#{report['hint'] ? " (#{report['hint']})" : ''}"
|
|
216
216
|
end
|
|
217
217
|
when "release"
|
|
218
218
|
result = Lock.release(dir, session: key)
|
|
219
|
-
if result == :not_owner
|
|
220
|
-
warn "plastic-lock: not the owner; run plastic-lock status"
|
|
221
|
-
exit 1
|
|
222
|
-
end
|
|
219
|
+
refuse "not the owner; plastic auto lock status #{intent_id} shows who is" if result == :not_owner
|
|
223
220
|
puts "released (#{result})"
|
|
224
221
|
when "reclaim"
|
|
225
222
|
status, lock_data = Lock.takeover(dir, session: key, harness: harness,
|
|
226
223
|
agent: opts[:agent], model: opts[:model],
|
|
227
224
|
thread: opts[:thread], run_mode: opts[:mode])
|
|
228
225
|
if status == :fresh
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
exit 1
|
|
226
|
+
refuse "lock is FRESH and held by #{lock_data['owner_session']}; " \
|
|
227
|
+
"back off (no silent reclaim)"
|
|
232
228
|
end
|
|
233
229
|
report = Arm.repair(intent_dir: dir, session: key, home: home, harness: harness,
|
|
234
230
|
agent: opts[:agent], model: opts[:model], thread: opts[:thread],
|
|
235
|
-
run_mode: opts[:mode]
|
|
231
|
+
run_mode: opts[:mode])
|
|
236
232
|
puts JSON.pretty_generate(report)
|
|
237
233
|
when "delegate"
|
|
238
234
|
usage! if opts[:delegate].nil?
|
package/scripts/project-links
CHANGED
|
@@ -48,10 +48,6 @@ require_relative "lib/revisions_writer"
|
|
|
48
48
|
class ProjectLinks
|
|
49
49
|
DEFAULT_HOME = File.join(Dir.home, ".plastic")
|
|
50
50
|
|
|
51
|
-
# The 72 intent dir audit destination (relative to plastic_home).
|
|
52
|
-
DEFAULT_AUDIT_REL =
|
|
53
|
-
"projects/plastic/store/72--links-graph-projection/resources/audit--links-projection.md"
|
|
54
|
-
|
|
55
51
|
def initialize(plastic_home: DEFAULT_HOME, dry_run: false, audit_path: nil,
|
|
56
52
|
drop_unbacked_links: false, intent: nil, store: nil)
|
|
57
53
|
@plastic_home = plastic_home
|
|
@@ -60,20 +56,7 @@ class ProjectLinks
|
|
|
60
56
|
@intent = intent && intent.to_s
|
|
61
57
|
@store = store
|
|
62
58
|
|
|
63
|
-
|
|
64
|
-
# the plan). With no explicit --audit-path, a dry run writes a `.dry-run.md`
|
|
65
|
-
# sibling, leaving the canonical real-run audit untouched. An explicit
|
|
66
|
-
# --audit-path is always honored verbatim (tests inject it). Mirrors
|
|
67
|
-
# RebuildGraph's discipline exactly.
|
|
68
|
-
canonical = File.join(plastic_home, DEFAULT_AUDIT_REL)
|
|
69
|
-
@audit_path =
|
|
70
|
-
if audit_path
|
|
71
|
-
audit_path
|
|
72
|
-
elsif dry_run
|
|
73
|
-
canonical.sub(/\.md\z/, ".dry-run.md")
|
|
74
|
-
else
|
|
75
|
-
canonical
|
|
76
|
-
end
|
|
59
|
+
@audit_path = audit_path || File.join(Plastic::StoreLayout.global_root(plastic_home), "resources", "audit--links-projection.md")
|
|
77
60
|
end
|
|
78
61
|
|
|
79
62
|
attr_reader :plastic_home, :dry_run, :audit_path, :drop_unbacked_links, :intent, :store
|
|
@@ -330,9 +313,11 @@ class ProjectLinks
|
|
|
330
313
|
"(ambiguous: multiple ## Links headings)"
|
|
331
314
|
end
|
|
332
315
|
|
|
333
|
-
#
|
|
316
|
+
# Return the audit for previews; save it only when applying changes.
|
|
334
317
|
def emit_audit(store_list, results)
|
|
335
318
|
text = render_audit(store_list, results)
|
|
319
|
+
return text if dry_run
|
|
320
|
+
|
|
336
321
|
FileUtils.mkdir_p(File.dirname(audit_path))
|
|
337
322
|
File.write(audit_path, text)
|
|
338
323
|
text
|
|
@@ -348,7 +333,7 @@ class ProjectLinks
|
|
|
348
333
|
|
|
349
334
|
def render_audit(store_list, results)
|
|
350
335
|
lines = []
|
|
351
|
-
lines << "# Audit: store-wide
|
|
336
|
+
lines << "# Audit: store-wide Links projection"
|
|
352
337
|
lines << ""
|
|
353
338
|
lines << "Generated: #{Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ")}#{dry_run ? " (DRY RUN)" : ""}"
|
|
354
339
|
lines << "Scoped to intent #{intent} only (--intent)." if intent
|
|
@@ -482,6 +467,6 @@ if $PROGRAM_NAME == __FILE__
|
|
|
482
467
|
"regenerated #{totals[:regenerated]}, added #{totals[:added]}, " \
|
|
483
468
|
"unchanged #{totals[:unchanged]}, failed #{totals[:failed]} " \
|
|
484
469
|
"across #{results.size} store(s)."
|
|
485
|
-
puts "Audit: #{tool.audit_path}"
|
|
470
|
+
puts(dry ? "Dry run: no files written." : "Audit: #{tool.audit_path}")
|
|
486
471
|
exit 1 if tool.any_failed?(results)
|
|
487
472
|
end
|
package/scripts/roadmap-graph
CHANGED
|
@@ -62,6 +62,7 @@ module RoadmapGraphCli
|
|
|
62
62
|
end
|
|
63
63
|
if result[:cycle]
|
|
64
64
|
warn "roadmap-graph check: cyclic graph: #{result[:cycle].join(' > ')}"
|
|
65
|
+
warn_dangling(result[:dangling])
|
|
65
66
|
return 1
|
|
66
67
|
end
|
|
67
68
|
if result[:reason]
|
|
@@ -73,14 +74,19 @@ module RoadmapGraphCli
|
|
|
73
74
|
puts "Ready: #{result[:ready].join(', ')}"
|
|
74
75
|
puts "Dead ends: #{result[:dead_ends].join(', ')}" unless result[:dead_ends].empty?
|
|
75
76
|
|
|
76
|
-
|
|
77
|
-
warn "roadmap-graph check: graph names #{result[:dangling].map(&:inspect).join(', ')}, no batch entry"
|
|
78
|
-
return 1
|
|
79
|
-
end
|
|
77
|
+
return 1 if warn_dangling(result[:dangling])
|
|
80
78
|
|
|
81
79
|
0
|
|
82
80
|
end
|
|
83
81
|
|
|
82
|
+
# Names every graph id no batch lists; true when there was one.
|
|
83
|
+
def warn_dangling(dangling)
|
|
84
|
+
return false if dangling.to_a.empty?
|
|
85
|
+
|
|
86
|
+
warn "roadmap-graph check: graph names #{dangling.map(&:inspect).join(', ')}, no batch entry"
|
|
87
|
+
true
|
|
88
|
+
end
|
|
89
|
+
|
|
84
90
|
def render(path, dry_run)
|
|
85
91
|
result = RoadmapRender.write(path, dry_run: dry_run)
|
|
86
92
|
unless result[:ok]
|
package/scripts/rollback.rb
CHANGED
|
@@ -89,12 +89,16 @@ class Rollback < InstallerCore
|
|
|
89
89
|
end
|
|
90
90
|
|
|
91
91
|
def switch_to(target, current)
|
|
92
|
+
return 3 if refuse_incompatible_store_layout(target)
|
|
93
|
+
|
|
92
94
|
action = action_for(target, current)
|
|
93
95
|
puts "#{action == "downgrade" ? "\u{23ea}" : "\u{23e9}"} #{current} \u{2192} #{target} (#{action})"
|
|
94
96
|
prepare_switch(target, current)
|
|
95
97
|
cmd = ["npx", "#{PKG}@#{target}", "install", "--reinstall", "--ledger-action", action, *harness_flags]
|
|
96
98
|
puts " $ #{cmd.join(" ")}"
|
|
97
|
-
system(*cmd)
|
|
99
|
+
return 1 unless system({"PLASTIC_PACKAGE_ROOT" => nil}, *cmd)
|
|
100
|
+
|
|
101
|
+
installed_version == target ? 0 : 1
|
|
98
102
|
end
|
|
99
103
|
|
|
100
104
|
# Before handing off to an older package (a downgrade), strip Plastic's own current hook
|
package/scripts/update.rb
CHANGED
|
@@ -62,6 +62,8 @@ class Update < InstallerCore
|
|
|
62
62
|
warn "No published version on the #{requested} channel."
|
|
63
63
|
return 1
|
|
64
64
|
when :ok
|
|
65
|
+
return 3 if refuse_incompatible_store_layout(res[:target])
|
|
66
|
+
|
|
65
67
|
if res[:kind] == :cross_bleeding && !confirm_bleeding(iv, res[:target], argv)
|
|
66
68
|
puts "Aborted."
|
|
67
69
|
return 1
|
|
@@ -235,10 +237,14 @@ class Update < InstallerCore
|
|
|
235
237
|
# the target version's install verb, recording the ledger action as `update`. On
|
|
236
238
|
# success, commits the re-synced core files and clears the update-check cache
|
|
237
239
|
# (former update skill, lines 124-125).
|
|
238
|
-
def perform_switch(target, agent_flags, switch_runner: ->(cmd) { system(*cmd) })
|
|
240
|
+
def perform_switch(target, agent_flags, switch_runner: ->(cmd) { system({"PLASTIC_PACKAGE_ROOT" => nil}, *cmd) })
|
|
239
241
|
cmd = ["npx", "#{PKG}@#{target}", "install", "--reinstall", "--ledger-action", "update", *agent_flags]
|
|
240
242
|
puts " $ #{cmd.join(" ")}"
|
|
241
243
|
return 1 unless switch_runner.call(cmd)
|
|
244
|
+
unless installed_version == target
|
|
245
|
+
warn "Update did not install #{target}; installed version is #{installed_version.inspect}."
|
|
246
|
+
return 1
|
|
247
|
+
end
|
|
242
248
|
|
|
243
249
|
commit_core_files(target)
|
|
244
250
|
clear_update_check_cache
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
## Numbered Decision Tables
|
|
2
2
|
|
|
3
3
|
The shared procedure for collecting owner rulings during any stage (Why, How, Exec).
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
An agent that needs the owner to choose between options or rule on a batch of open
|
|
5
|
+
questions follows this procedure instead of improvising its own format.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
The installer copies this file to `~/.plastic/_decision-tables.md`.
|
|
8
8
|
|
|
9
9
|
### The procedure
|
|
10
10
|
|