@zalom/plastic 1.14.0 → 2.0.0-alpha.1
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 +101 -294
- package/README.md +5 -6
- package/agents/plastic-advisor.md +18 -17
- package/agents/plastic-enforcer.md +57 -69
- package/agents/plastic-executor.md +34 -20
- package/agents/plastic-faux-advisor.md +18 -18
- package/config_asks.yml +38 -0
- package/hooks/capture +4 -0
- package/hooks/check-update +12 -3
- package/hooks/close +5 -0
- package/hooks/hooks.json +9 -49
- package/hooks/record +4 -0
- package/package.json +3 -2
- package/scripts/agent-report +8 -9
- package/scripts/append-ledger +195 -0
- package/scripts/codex-hook +101 -95
- package/scripts/dashboard.rb +4 -4
- package/scripts/doctor.rb +106 -43
- package/scripts/end-intent +77 -76
- package/scripts/exec-worktree +11 -20
- package/scripts/file-session-intent +61 -0
- package/scripts/hook-capture +242 -0
- package/scripts/hook-close +34 -0
- package/scripts/hook-record +149 -0
- package/scripts/hook-session-start +95 -12
- package/scripts/install.rb +8 -6
- package/scripts/lib/agent_models.rb +1 -7
- package/scripts/lib/arm.rb +283 -0
- package/scripts/lib/backfill_intent.rb +316 -0
- package/scripts/lib/bridge.rb +24 -1597
- package/scripts/lib/doctor_core.rb +17 -88
- package/scripts/lib/doctor_session_ledger.rb +106 -0
- package/scripts/lib/exec_worktree.rb +14 -60
- package/scripts/lib/harness_text.rb +5 -4
- package/scripts/lib/hook_registry.rb +44 -107
- package/scripts/lib/insights.rb +3 -4
- package/scripts/lib/installer_core.rb +19 -30
- package/scripts/lib/lock.rb +6 -26
- package/scripts/lib/outcome_guard.rb +7 -5
- package/scripts/lib/project_validator.rb +30 -1
- package/scripts/lib/roadmap_queue.rb +3 -3
- package/scripts/lib/roadmap_savepoint.rb +1 -1
- package/scripts/lib/rule_catalog.rb +6 -3
- package/scripts/lib/savepoint.rb +320 -0
- package/scripts/lib/scaffold_intent.rb +13 -179
- package/scripts/lib/session_backfill.rb +184 -0
- package/scripts/lib/session_close.rb +84 -0
- package/scripts/lib/session_git.rb +469 -0
- package/scripts/lib/session_ledger.rb +497 -0
- package/scripts/lib/store_discovery.rb +1 -1
- package/scripts/lib/store_provisioning.rb +1 -1
- package/scripts/maintenance-run +7 -7
- package/scripts/new-intent +63 -4
- package/scripts/plastic-lock +94 -41
- package/scripts/promote-session-item +150 -0
- package/scripts/rebuild-graph +1 -1
- package/scripts/roadmap-next +2 -2
- package/scripts/rollback.rb +42 -3
- package/scripts/scaffold-intent +37 -63
- package/scripts/session-commit +116 -0
- package/scripts/spawn-preamble +5 -5
- package/scripts/update.rb +7 -3
- package/skills/agent-advisor/SKILL.md +3 -3
- package/skills/agent-advisor/references/advisor-protocol.md +20 -20
- package/skills/auto/SKILL.md +218 -317
- package/skills/auto/evals/evals.json +10 -10
- package/skills/auto/references/agent-architecture.md +102 -90
- package/skills/auto/references/agent-report-contract.md +4 -21
- package/skills/auto/references/end-tail.md +47 -53
- package/skills/auto/references/human-report-contract.md +8 -8
- package/skills/conventions/SKILL.md +1 -3
- package/skills/conventions/references/lifecycle-and-savepoints.md +35 -1
- package/skills/conventions/references/locks-and-worktrees.md +47 -55
- package/skills/conventions/references/maintenance-and-revisions.md +12 -12
- package/skills/dashboard/SKILL.md +1 -1
- package/skills/direct/SKILL.md +66 -0
- package/skills/direct/references/request-signals.md +59 -0
- package/skills/doctor/SKILL.md +58 -11
- package/skills/doctor/report.md +3 -1
- package/skills/install/SKILL.md +11 -9
- package/skills/intent-continuing/SKILL.md +131 -89
- package/skills/intent-continuing/references/boarding-matrix.md +34 -0
- package/skills/intent-continuing/references/context-management.md +2 -2
- package/skills/intent-creating/SKILL.md +2 -2
- package/skills/intent-creating/references/lifecycle.md +1 -1
- package/skills/intent-ending/SKILL.md +28 -37
- package/skills/intent-ending/evals/evals.json +4 -4
- package/skills/intent-executing/SKILL.md +12 -49
- package/skills/intent-executing/plan-reviewer-prompt.md +39 -0
- package/skills/intent-speccing/SKILL.md +108 -51
- package/skills/intent-speccing/evals/evals.json +5 -5
- package/skills/intent-speccing/references/per-section-fill-rules.md +4 -4
- package/skills/intent-speccing/references/self-verify-checklist.md +1 -2
- package/skills/project-creating/SKILL.md +1 -1
- package/skills/releasing/SKILL.md +7 -8
- package/skills/tutorial/SKILL.md +2 -2
- package/skills/tutorial/references/track-1-guided.md +16 -18
- package/skills/tutorial/references/track-2-auto.md +12 -13
- package/skills/tutorial/references/track-3-projects-and-roadmaps.md +2 -2
- package/templates/agents.md +4 -7
- package/templates/project.yml +7 -0
- package/templates/roadmap.md +1 -1
- package/templates/savepoint.md +2 -2
- package/templates/session-intent.md +29 -0
- package/agents/plastic-brainstorming.md +0 -48
- package/agents/plastic-future-intent-researcher.md +0 -35
- package/agents/plastic-intent-curator.md +0 -47
- package/agents/plastic-intent-discovery.md +0 -43
- package/agents/plastic-planner.md +0 -56
- package/agents/plastic-spec-specialist.md +0 -53
- package/hooks/auto-arm +0 -5
- package/hooks/bash-gate +0 -3
- package/hooks/continue +0 -31
- package/hooks/edit-gates +0 -3
- package/hooks/future-intent-check +0 -25
- package/hooks/gate-check +0 -12
- package/hooks/power-tools +0 -8
- package/scripts/hook-auto-arm +0 -51
- package/scripts/hook-bash-gate +0 -78
- package/scripts/hook-code-gate +0 -38
- package/scripts/hook-continue +0 -48
- package/scripts/hook-create-gate +0 -51
- package/scripts/hook-edit-gates +0 -58
- package/scripts/hook-future-intent-check +0 -90
- package/scripts/hook-gate-check +0 -169
- package/scripts/hook-links-gate +0 -54
- package/scripts/hook-lock-gate +0 -47
- package/scripts/hook-power-tools +0 -38
- package/scripts/hook-savepoint-pre +0 -32
- package/scripts/lib/codex_edit_gates.rb +0 -138
- package/scripts/lib/edit_gates.rb +0 -398
- package/scripts/lib/links_gate.rb +0 -140
- package/scripts/lib/qmd_hook.rb +0 -41
- package/scripts/lib/spec_header.rb +0 -83
- package/scripts/lib/start_intent.rb +0 -296
- package/scripts/start-intent +0 -89
- package/skills/_active-intent-gate.md +0 -26
- package/skills/auto/references/tiers.md +0 -100
- package/skills/continuing/SKILL.md +0 -34
- package/skills/continuing/evals/evals.json +0 -91
- package/skills/conventions/references/gates-and-enforcement.md +0 -53
- package/skills/conventions/references/tiers-and-dispatch.md +0 -135
- package/skills/doctor/references/gates-stuck-detection.md +0 -43
- package/skills/intent-brainstorming/SKILL.md +0 -118
- package/skills/intent-brainstorming/evals/evals.json +0 -67
- package/skills/intent-continuing/evals/evals.json +0 -145
- package/skills/intent-discovering/SKILL.md +0 -56
- package/skills/intent-grilling/SKILL.md +0 -108
- package/skills/intent-linking/SKILL.md +0 -128
- package/skills/intent-linking/evals/evals.json +0 -22
- package/skills/intent-linking/references/zettelkasten.md +0 -45
- package/skills/intent-locking/SKILL.md +0 -64
- package/skills/intent-planning/SKILL.md +0 -145
- package/skills/intent-planning/evals/evals.json +0 -81
- package/skills/intent-planning/references/plan-format.md +0 -130
- package/skills/intent-researching/SKILL.md +0 -123
- package/skills/intent-researching/evals/evals.json +0 -22
- package/skills/intent-savepoint/SKILL.md +0 -83
- package/skills/intent-savepoint/references/context-management.md +0 -32
- package/skills/intent-starting/SKILL.md +0 -151
- package/skills/intent-starting/evals/evals.json +0 -117
- package/skills/intent-starting/references/boarding-matrix.md +0 -35
- package/skills/project-continuing/SKILL.md +0 -119
- package/skills/project-continuing/evals/evals.json +0 -100
- package/skills/roadmap-continuing/SKILL.md +0 -89
- package/skills/roadmap-continuing/evals/evals.json +0 -82
- package/skills/skill-creating/SKILL.md +0 -75
- package/skills/skill-creating/evals/evals.json +0 -108
- package/skills/skill-creating/references/agents.md +0 -168
- package/skills/skill-creating/references/defaults-first.md +0 -23
- package/skills/skill-creating/references/evals.md +0 -41
- package/skills/skill-creating/references/hooks.md +0 -251
- package/skills/skill-creating/references/progressive-disclosure.md +0 -176
- package/skills/skill-creating/references/scripts.md +0 -166
- package/skills/skill-creating/references/skills.md +0 -169
- package/skills/skill-creating/scripts/scaffold.rb +0 -313
- package/skills/skill-evaluating/SKILL.md +0 -141
- package/skills/skill-evaluating/assets/eval-template.json +0 -12
- package/skills/skill-evaluating/evals/evals.json +0 -75
- package/skills/skill-evaluating/references/convention-checks.md +0 -76
- package/skills/skill-evaluating/references/eval-methodology.md +0 -154
- package/skills/store-curating/SKILL.md +0 -64
- package/skills/store-curating/evals/evals.json +0 -38
- package/skills/store-indexing/SKILL.md +0 -93
- package/skills/store-indexing/evals/evals.json +0 -22
- package/skills/store-indexing/references/zettelkasten-linking.md +0 -32
- package/skills/store-provisioning/SKILL.md +0 -55
- /package/skills/{project-continuing → intent-continuing}/references/board-fill.md +0 -0
- /package/skills/{roadmap-continuing → intent-continuing}/references/liveness-ranking.md +0 -0
- /package/skills/{intent-brainstorming → intent-speccing}/references/design-principles.md +0 -0
package/scripts/plastic-lock
CHANGED
|
@@ -2,19 +2,29 @@
|
|
|
2
2
|
# encoding: UTF-8
|
|
3
3
|
# frozen_string_literal: true
|
|
4
4
|
#
|
|
5
|
-
# plastic-lock: inspect and
|
|
6
|
-
# and take/free per-artifact claim tokens
|
|
5
|
+
# plastic-lock: take, inspect, repair, and give back the durable delivery lock
|
|
6
|
+
# (intent 108, D5; intent 307), and take/free per-artifact claim tokens
|
|
7
|
+
# (intent 111 D1/D5).
|
|
7
8
|
#
|
|
8
|
-
# Usage: plastic-lock <status|who|fix|release|reclaim|delegate|claim|release-claim>
|
|
9
|
+
# Usage: plastic-lock <arm|status|who|fix|release|reclaim|delegate|claim|release-claim>
|
|
9
10
|
# [--intent-dir DIR] [--session SID] [--delegate SID] [--artifact NAME]
|
|
11
|
+
# [--harness claude|codex] [--agent NAME] [--model MODEL] [--thread ID]
|
|
12
|
+
# [--mode auto|guided] [--status finished|failed]
|
|
10
13
|
#
|
|
11
14
|
# Verbs:
|
|
15
|
+
# arm take the intent for this session (intent 307): acquire
|
|
16
|
+
# delivery.lock stamped with --mode (default auto) and the
|
|
17
|
+
# provenance flags, provision the code worktree, and point the
|
|
18
|
+
# session at the intent; exit 1 on a held, stale, excluded, or
|
|
19
|
+
# corrupt lock with the resolving doctor hint
|
|
12
20
|
# who compact, read-only durable owner/activity view
|
|
13
|
-
# status report the lock file,
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
21
|
+
# status report the lock file, its freshness, the derived worktree,
|
|
22
|
+
# whether this session's pointer names the intent, and any
|
|
23
|
+
# live per-artifact claims
|
|
24
|
+
# fix idempotent repair of the lock for the current session; never
|
|
25
|
+
# touches a fresh foreign lock
|
|
26
|
+
# release owner clears the lock and points the session back at the day
|
|
27
|
+
# ledger (End tail / abandoning a boarding)
|
|
18
28
|
# reclaim explicit takeover of a stale lock, audited in savepoint.md
|
|
19
29
|
# delegate owner registers a subagent session under the lock (D4)
|
|
20
30
|
# claim acquire the named --artifact's claim token (intent 111);
|
|
@@ -22,15 +32,23 @@
|
|
|
22
32
|
# including this same session; takes over a stale claim
|
|
23
33
|
# release-claim free the named --artifact's claim token
|
|
24
34
|
#
|
|
25
|
-
# Without --intent-dir the intent is resolved from this session's
|
|
26
|
-
#
|
|
35
|
+
# Without --intent-dir the intent is resolved from this session's pointer
|
|
36
|
+
# (~/.plastic/store/.tmp/<session>/current) when it names an intent id, looked
|
|
37
|
+
# up in the working directory's project store and then the global store. The
|
|
38
|
+
# /tmp bridge JSON this CLI used to resolve through was removed in 2.0
|
|
39
|
+
# (intent 307). Exit 0 on success/report; exit 1 when the verb is blocked.
|
|
27
40
|
|
|
28
41
|
require "json"
|
|
42
|
+
require "yaml"
|
|
29
43
|
require_relative "lib/bridge"
|
|
30
44
|
require_relative "lib/lock"
|
|
45
|
+
require_relative "lib/arm"
|
|
46
|
+
require_relative "lib/session_ledger"
|
|
47
|
+
|
|
48
|
+
VERBS = %w[arm status who fix release reclaim delegate claim release-claim].freeze
|
|
31
49
|
|
|
32
50
|
def usage!
|
|
33
|
-
warn "usage: plastic-lock
|
|
51
|
+
warn "usage: plastic-lock <#{VERBS.join('|')}> " \
|
|
34
52
|
"[--intent-dir DIR] [--session SID] [--delegate SID] [--artifact NAME] " \
|
|
35
53
|
"[--harness claude|codex] [--agent NAME] [--model MODEL] [--thread ID] " \
|
|
36
54
|
"[--mode auto|guided] [--status finished|failed]"
|
|
@@ -38,7 +56,7 @@ def usage!
|
|
|
38
56
|
end
|
|
39
57
|
|
|
40
58
|
verb = ARGV.shift
|
|
41
|
-
usage! unless
|
|
59
|
+
usage! unless VERBS.include?(verb)
|
|
42
60
|
|
|
43
61
|
opts = {}
|
|
44
62
|
until ARGV.empty?
|
|
@@ -64,15 +82,29 @@ session = opts[:session]
|
|
|
64
82
|
session = ENV["CLAUDE_CODE_SESSION_ID"] if session.nil? || session.strip.empty?
|
|
65
83
|
harness = opts[:harness]
|
|
66
84
|
hint_harness = harness || "claude"
|
|
85
|
+
# The plastic home every hook uses (PLASTIC_HOME, else ~/.plastic); Arm takes the
|
|
86
|
+
# OS home above it, the same convention Worktree resolves projects.yml from.
|
|
87
|
+
plastic_home_env = ENV["PLASTIC_HOME"].to_s.strip
|
|
88
|
+
home = plastic_home_env.empty? ? Dir.home : File.dirname(File.expand_path(plastic_home_env))
|
|
89
|
+
|
|
90
|
+
# The stores this session's pointer is resolved against: the project store of
|
|
91
|
+
# the working directory (projects.yml, longest path match), then the global one.
|
|
92
|
+
def candidate_stores(home, cwd)
|
|
93
|
+
plastic_home = File.join(home, ".plastic")
|
|
94
|
+
stores = []
|
|
95
|
+
slug = SessionLedger.project_slug(cwd, plastic_home: plastic_home)
|
|
96
|
+
stores << File.join(plastic_home, "projects", slug, "store") if slug && slug != "global"
|
|
97
|
+
stores << File.join(plastic_home, "store")
|
|
98
|
+
stores
|
|
99
|
+
end
|
|
67
100
|
|
|
68
101
|
dir = opts[:dir]
|
|
69
102
|
if dir.nil? || dir.strip.empty?
|
|
70
|
-
if verb
|
|
71
|
-
warn "plastic-lock:
|
|
103
|
+
if %w[who arm].include?(verb)
|
|
104
|
+
warn "plastic-lock: #{verb} needs --intent-dir <intent dir>"
|
|
72
105
|
exit 1
|
|
73
106
|
end
|
|
74
|
-
|
|
75
|
-
dir = Bridge.bridge_intent_dir(bridge)
|
|
107
|
+
dir = Arm.intent_dir_from_pointer(session, home: home, stores: candidate_stores(home, Dir.pwd))
|
|
76
108
|
end
|
|
77
109
|
if dir.nil?
|
|
78
110
|
warn "plastic-lock: no intent resolved; pass --intent-dir <intent dir>"
|
|
@@ -85,7 +117,7 @@ if verb == "who"
|
|
|
85
117
|
basename = File.basename(dir)
|
|
86
118
|
intent_id, slug = basename.split("--", 2)
|
|
87
119
|
display = ->(value) { value.nil? || value.to_s.strip.empty? || value == "unknown" ? "Unknown" : value.to_s }
|
|
88
|
-
|
|
120
|
+
harness_name = ->(value) {
|
|
89
121
|
shown = display.call(value)
|
|
90
122
|
shown == "Unknown" ? shown : shown.sub(/\A./) { |char| char.upcase }
|
|
91
123
|
}
|
|
@@ -99,12 +131,12 @@ if verb == "who"
|
|
|
99
131
|
|
|
100
132
|
puts "#{intent_id} · #{slug || basename}"
|
|
101
133
|
puts "State: #{view['state']}"
|
|
102
|
-
puts "Controller: #{display.call(owner['agent'])} via #{
|
|
134
|
+
puts "Controller: #{display.call(owner['agent'])} via #{harness_name.call(owner['harness'])}"
|
|
103
135
|
puts "Session: #{display.call(view['owner_session'])}"
|
|
104
136
|
puts "Heartbeat: #{view['heartbeat_at'] || 'none'}"
|
|
105
137
|
puts "Claims: #{claim_text.empty? ? 'none' : claim_text.join(', ')}"
|
|
106
138
|
if delegate
|
|
107
|
-
puts "Delegate: #{display.call(delegate['agent'])} via #{
|
|
139
|
+
puts "Delegate: #{display.call(delegate['agent'])} via #{harness_name.call(delegate['harness'])}, #{display.call(delegate['status'])}"
|
|
108
140
|
else
|
|
109
141
|
puts "Delegate: none"
|
|
110
142
|
end
|
|
@@ -113,31 +145,59 @@ end
|
|
|
113
145
|
|
|
114
146
|
intent_id = Bridge.intent_id_from_dir(dir)
|
|
115
147
|
store = File.dirname(dir)
|
|
116
|
-
|
|
117
|
-
|
|
148
|
+
key = Arm.resolve_session(session, store: store, intent_id: intent_id)
|
|
149
|
+
|
|
150
|
+
def doctor_hint(verb_text, harness)
|
|
151
|
+
"run #{Lock.skill_ref('plastic-doctor', harness: harness)} #{verb_text}"
|
|
152
|
+
end
|
|
118
153
|
|
|
119
154
|
case verb
|
|
155
|
+
when "arm"
|
|
156
|
+
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
|
+
case result[:status]
|
|
159
|
+
when :acquired, :owned
|
|
160
|
+
wt = result[:worktree] || {}
|
|
161
|
+
puts JSON.pretty_generate(
|
|
162
|
+
"intent_dir" => dir, "session" => result[:session], "status" => result[:status].to_s,
|
|
163
|
+
"run_mode" => result[:lock] && result[:lock]["run_mode"],
|
|
164
|
+
"worktree" => wt, "pointer" => result[:pointer]
|
|
165
|
+
)
|
|
166
|
+
when :held
|
|
167
|
+
warn "plastic-lock: delivery lock for intent #{intent_id} is held by session " \
|
|
168
|
+
"#{result[:lock] && result[:lock]['owner_session']}; #{doctor_hint('check the lock status', hint_harness)}"
|
|
169
|
+
exit 1
|
|
170
|
+
when :stale
|
|
171
|
+
warn "plastic-lock: delivery lock for intent #{intent_id} is stale (owner " \
|
|
172
|
+
"#{result[:lock] && result[:lock]['owner_session']}); #{doctor_hint('reclaim the lock to take it over with an audit', hint_harness)}"
|
|
173
|
+
exit 1
|
|
174
|
+
when :excluded
|
|
175
|
+
warn "plastic-lock: a #{result[:lock] && result[:lock]['type']} lock is active on intent " \
|
|
176
|
+
"#{intent_id}; #{doctor_hint('check the lock status', hint_harness)}"
|
|
177
|
+
exit 1
|
|
178
|
+
when :corrupt
|
|
179
|
+
warn "plastic-lock: delivery.lock for intent #{intent_id} is unreadable; #{doctor_hint('fix the lock', hint_harness)}"
|
|
180
|
+
exit 1
|
|
181
|
+
end
|
|
120
182
|
when "status"
|
|
121
183
|
lock = Lock.read(dir)
|
|
122
|
-
|
|
184
|
+
pointer = Arm.read_pointer(key, home: home)
|
|
123
185
|
report = {
|
|
124
186
|
"intent_dir" => dir,
|
|
125
187
|
"session" => key,
|
|
126
188
|
"lock" => lock,
|
|
127
189
|
"lock_fresh" => Lock.fresh?(dir),
|
|
128
190
|
"lock_corrupt" => Lock.corrupt?(dir),
|
|
129
|
-
"
|
|
130
|
-
"
|
|
131
|
-
|
|
191
|
+
"worktree" => Arm.worktree_block(intent_dir: dir, home: home),
|
|
192
|
+
"pointer" => pointer,
|
|
193
|
+
"pointer_names_intent" => pointer == intent_id,
|
|
132
194
|
"claims" => Claim.claims_status(dir),
|
|
133
195
|
}
|
|
134
196
|
puts JSON.pretty_generate(report)
|
|
135
197
|
when "fix"
|
|
136
|
-
report =
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
run_mode: opts[:mode],
|
|
140
|
-
hint_harness: hint_harness)
|
|
198
|
+
report = Arm.repair(intent_dir: dir, session: key, home: home, harness: harness,
|
|
199
|
+
agent: opts[:agent], model: opts[:model], thread: opts[:thread],
|
|
200
|
+
run_mode: opts[:mode], hint_harness: hint_harness)
|
|
141
201
|
puts JSON.pretty_generate(report)
|
|
142
202
|
unless report["status"] == "repaired"
|
|
143
203
|
warn "plastic-lock: #{report['status']} by #{report['owner']}" \
|
|
@@ -150,12 +210,7 @@ when "release"
|
|
|
150
210
|
warn "plastic-lock: not the owner; run plastic-lock status"
|
|
151
211
|
exit 1
|
|
152
212
|
end
|
|
153
|
-
|
|
154
|
-
if data
|
|
155
|
-
data["lock"] = { "owner_session" => nil, "acquired_at" => nil,
|
|
156
|
-
"host" => nil, "type" => nil, "delegates" => [] }
|
|
157
|
-
Bridge.write(key, data)
|
|
158
|
-
end
|
|
213
|
+
Arm.reset_pointer(key, intent_id, home: home)
|
|
159
214
|
puts "released (#{result})"
|
|
160
215
|
when "reclaim"
|
|
161
216
|
status, lock_data = Lock.takeover(dir, session: key, harness: harness,
|
|
@@ -166,11 +221,9 @@ when "reclaim"
|
|
|
166
221
|
"back off (no silent reclaim)"
|
|
167
222
|
exit 1
|
|
168
223
|
end
|
|
169
|
-
report =
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
run_mode: opts[:mode],
|
|
173
|
-
hint_harness: hint_harness)
|
|
224
|
+
report = Arm.repair(intent_dir: dir, session: key, home: home, harness: harness,
|
|
225
|
+
agent: opts[:agent], model: opts[:model], thread: opts[:thread],
|
|
226
|
+
run_mode: opts[:mode], hint_harness: hint_harness)
|
|
174
227
|
puts JSON.pretty_generate(report)
|
|
175
228
|
when "delegate"
|
|
176
229
|
usage! if opts[:delegate].nil?
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# encoding: UTF-8
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
# promote-session-item (intent 301): turn one session day-ledger line into a
|
|
6
|
+
# registered intent through scripts/new-intent (never hand-authored), record
|
|
7
|
+
# the day of origin in the new intent, and mark the line promoted.
|
|
8
|
+
#
|
|
9
|
+
# Usage:
|
|
10
|
+
# promote-session-item --day <YYYYMMDD> --match <substring> [--session <id>]
|
|
11
|
+
# [--store <dir>] [--project <slug>] [--templates <dir>]
|
|
12
|
+
# [--plastic-home <dir>] [--no-reindex]
|
|
13
|
+
#
|
|
14
|
+
# Target: the newest open line, else the newest pending line, whose summary
|
|
15
|
+
# contains --match, tagged with --session when given, any session otherwise.
|
|
16
|
+
# The intent lands in the project's store when --project names a registered
|
|
17
|
+
# project, else in --store ($PLASTIC_HOME/store by default). No graph edge to
|
|
18
|
+
# the day id is written (the day ledger is not a store node); the origin is
|
|
19
|
+
# the `session_day:` frontmatter key plus a line under ## Context.
|
|
20
|
+
#
|
|
21
|
+
# Exit codes: 0 promoted (prints the intent dir last), 1 new-intent failed,
|
|
22
|
+
# 2 usage error or nothing to promote.
|
|
23
|
+
|
|
24
|
+
require "open3"
|
|
25
|
+
require "yaml"
|
|
26
|
+
require_relative "lib/session_ledger"
|
|
27
|
+
require_relative "lib/session_backfill"
|
|
28
|
+
require_relative "lib/store_provisioning"
|
|
29
|
+
|
|
30
|
+
def usage_abort(message)
|
|
31
|
+
warn "promote-session-item: #{message}"
|
|
32
|
+
exit 2
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def next_value!(argv, i, flag)
|
|
36
|
+
value = argv[i + 1]
|
|
37
|
+
usage_abort("#{flag} needs a value") if value.nil? || value.start_with?("--")
|
|
38
|
+
value
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
opts = { day: nil, match: nil, session: nil, store: nil, project: nil, templates: nil,
|
|
42
|
+
plastic_home: nil, reindex: true }
|
|
43
|
+
i = 0
|
|
44
|
+
while i < ARGV.length
|
|
45
|
+
case ARGV[i]
|
|
46
|
+
when "--day" then opts[:day] = next_value!(ARGV, i, "--day"); i += 1
|
|
47
|
+
when "--match" then opts[:match] = next_value!(ARGV, i, "--match"); i += 1
|
|
48
|
+
when "--session" then opts[:session] = next_value!(ARGV, i, "--session"); i += 1
|
|
49
|
+
when "--store" then opts[:store] = next_value!(ARGV, i, "--store"); i += 1
|
|
50
|
+
when "--project" then opts[:project] = next_value!(ARGV, i, "--project"); i += 1
|
|
51
|
+
when "--templates" then opts[:templates] = next_value!(ARGV, i, "--templates"); i += 1
|
|
52
|
+
when "--plastic-home" then opts[:plastic_home] = next_value!(ARGV, i, "--plastic-home"); i += 1
|
|
53
|
+
when "--no-reindex" then opts[:reindex] = false
|
|
54
|
+
else usage_abort("unknown argument #{ARGV[i].inspect}")
|
|
55
|
+
end
|
|
56
|
+
i += 1
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
usage_abort("--day is required") if opts[:day].nil?
|
|
60
|
+
usage_abort("--day must be a real calendar date, YYYYMMDD") unless SessionLedger.valid_day_id?(opts[:day])
|
|
61
|
+
usage_abort("--match is required") if opts[:match].nil? || opts[:match].empty?
|
|
62
|
+
|
|
63
|
+
plastic_home = File.expand_path(opts[:plastic_home] || ENV.fetch("PLASTIC_HOME", "~/.plastic"))
|
|
64
|
+
store = opts[:store] || File.join(plastic_home, "store")
|
|
65
|
+
templates = opts[:templates] || File.expand_path("../templates", __dir__)
|
|
66
|
+
session = opts[:session] ? SessionLedger.short_session_id(opts[:session]) : nil
|
|
67
|
+
|
|
68
|
+
target_store = store
|
|
69
|
+
if opts[:project]
|
|
70
|
+
projects = StoreProvisioning.load_projects(plastic_home)
|
|
71
|
+
usage_abort("--project #{opts[:project]} is not a registered project") unless projects.key?(opts[:project])
|
|
72
|
+
target_store = File.join(plastic_home, "projects", opts[:project], "store")
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# --- pick the target line ---------------------------------------------------
|
|
76
|
+
|
|
77
|
+
day_dir = SessionLedger.day_dir(store, opts[:day])
|
|
78
|
+
checklist = SessionLedger.checklist_path(store, opts[:day])
|
|
79
|
+
items = SessionBackfill.read_items(day_dir)
|
|
80
|
+
candidates = items.select do |item|
|
|
81
|
+
(session.nil? || item[:session] == session) && item[:summary].include?(opts[:match])
|
|
82
|
+
end
|
|
83
|
+
target = candidates.reverse.find { |item| item[:state] == :open } ||
|
|
84
|
+
candidates.reverse.find { |item| item[:state] == :pending }
|
|
85
|
+
usage_abort("nothing to promote in #{opts[:day]} matching #{opts[:match].inspect}") if target.nil?
|
|
86
|
+
|
|
87
|
+
# --- scaffold through new-intent ---------------------------------------------
|
|
88
|
+
|
|
89
|
+
def slug_for(summary)
|
|
90
|
+
words = summary.downcase.gsub(/[^a-z0-9]+/, "-").gsub(/\A-+|-+\z/, "").split("-").reject(&:empty?)
|
|
91
|
+
slug = words.first(5).join("-")
|
|
92
|
+
slug.empty? ? "item" : slug
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
new_intent = File.expand_path("new-intent", __dir__)
|
|
96
|
+
out, err, status = Open3.capture3({ "RUBYOPT" => nil }, RbConfig.ruby, new_intent,
|
|
97
|
+
"--store", target_store, "--intent", target[:summary],
|
|
98
|
+
"--slug", slug_for(target[:summary]), "--templates", templates)
|
|
99
|
+
unless status.success?
|
|
100
|
+
warn "promote-session-item: new-intent failed:"
|
|
101
|
+
warn err
|
|
102
|
+
warn out
|
|
103
|
+
exit 1
|
|
104
|
+
end
|
|
105
|
+
intent_dir = out.lines.last.to_s.strip
|
|
106
|
+
usage_abort("new-intent printed no intent dir") if intent_dir.empty? || !Dir.exist?(intent_dir)
|
|
107
|
+
intent_id = File.basename(intent_dir).split("--", 2).first
|
|
108
|
+
|
|
109
|
+
# --- record the origin in the new intent (one atomic rewrite) -----------------
|
|
110
|
+
|
|
111
|
+
intent_file = File.join(intent_dir, "#{File.basename(intent_dir)}.md")
|
|
112
|
+
content = File.read(intent_file)
|
|
113
|
+
parts = content.split(/^(---\s*\n)/, 3)
|
|
114
|
+
if parts.length >= 4
|
|
115
|
+
parts[2] += "session_day: \"#{opts[:day]}\"\n"
|
|
116
|
+
content = parts[0] + parts[1] + parts[2] + parts[3] + parts[4].to_s
|
|
117
|
+
end
|
|
118
|
+
origin = "Promoted from the session ledger of #{opts[:day]}: #{target[:summary]}\n"
|
|
119
|
+
content = content.sub(/^## Context\n/) { |heading| heading + origin }
|
|
120
|
+
SessionBackfill.write_atomic(intent_dir, File.basename(intent_file), content)
|
|
121
|
+
|
|
122
|
+
# --- register the intent under ## Future in the store's INDEX.md ----------------
|
|
123
|
+
|
|
124
|
+
def register_in_index(index_path, intent_id, intent_dir, summary)
|
|
125
|
+
return unless File.exist?(index_path)
|
|
126
|
+
|
|
127
|
+
relative = "store/#{File.basename(intent_dir)}/#{File.basename(intent_dir)}.md"
|
|
128
|
+
entry = "- [#{intent_id} - #{summary}](#{relative}) - promoted from a session ledger\n"
|
|
129
|
+
content = File.read(index_path)
|
|
130
|
+
return if content.include?("(#{relative})")
|
|
131
|
+
|
|
132
|
+
updated = content.sub(/^## Future\n(?:\(no future intents\)\n)?/) { |heading| heading.sub(/\(no future intents\)\n/, "") + entry }
|
|
133
|
+
SessionBackfill.write_atomic(File.dirname(index_path), File.basename(index_path), updated)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
register_in_index(File.join(File.dirname(target_store), "INDEX.md"), intent_id, intent_dir, target[:summary])
|
|
137
|
+
|
|
138
|
+
# --- mark the line, note it, reindex ---------------------------------------------
|
|
139
|
+
|
|
140
|
+
SessionLedger.set_state(checklist, from: target[:state], to: :promoted, session: target[:session],
|
|
141
|
+
match: target[:summary])
|
|
142
|
+
SessionLedger.append_line(SessionLedger.savepoint_path(store, opts[:day]),
|
|
143
|
+
SessionLedger.savepoint_line("Note", target[:session], target[:project],
|
|
144
|
+
"promoted \"#{target[:summary]}\" to intent #{intent_id}",
|
|
145
|
+
now: Time.now))
|
|
146
|
+
if opts[:reindex]
|
|
147
|
+
qmd_sync = File.expand_path("qmd-sync", __dir__)
|
|
148
|
+
system({ "RUBYOPT" => nil }, RbConfig.ruby, qmd_sync, "reindex", out: File::NULL, err: File::NULL) if File.exist?(qmd_sync)
|
|
149
|
+
end
|
|
150
|
+
puts intent_dir
|
package/scripts/rebuild-graph
CHANGED
|
@@ -82,7 +82,7 @@ class RebuildGraph
|
|
|
82
82
|
end
|
|
83
83
|
|
|
84
84
|
# Registered projects.yml slugs with no store directory on disk (legal;
|
|
85
|
-
# plastic-
|
|
85
|
+
# the plastic-doctor provisioning section exists for exactly this state). Reported in the audit
|
|
86
86
|
# (intent 189 D5) so this never looks like a silent empty store.
|
|
87
87
|
def missing_stores
|
|
88
88
|
discovery[:missing]
|
package/scripts/roadmap-next
CHANGED
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
|
|
5
5
|
# roadmap-next - thin CLI over RoadmapQueue (intent 148).
|
|
6
6
|
#
|
|
7
|
-
# The auto loop and plastic-
|
|
7
|
+
# The auto loop and plastic-intent-continuing both call this instead of ranking roadmap
|
|
8
8
|
# liveness in prose. Queue mode (default) is for the auto loop: it breaks a tie
|
|
9
9
|
# deterministically and reports the winner's frontier state. Which mode (--which) is for
|
|
10
|
-
# plastic-
|
|
10
|
+
# plastic-intent-continuing: it returns tie_candidates so the skill's roadmap route can resolve
|
|
11
11
|
# a genuine tie.
|
|
12
12
|
#
|
|
13
13
|
# Usage:
|
package/scripts/rollback.rb
CHANGED
|
@@ -82,8 +82,6 @@ class Rollback < InstallerCore
|
|
|
82
82
|
File.exist?(path) ? File.read(path).strip : nil
|
|
83
83
|
end
|
|
84
84
|
|
|
85
|
-
private
|
|
86
|
-
|
|
87
85
|
def flag_value(argv, name)
|
|
88
86
|
i = argv.index(name)
|
|
89
87
|
return nil unless i && argv[i + 1]
|
|
@@ -93,11 +91,52 @@ class Rollback < InstallerCore
|
|
|
93
91
|
def switch_to(target, current)
|
|
94
92
|
action = action_for(target, current)
|
|
95
93
|
puts "#{action == "downgrade" ? "\u{23ea}" : "\u{23e9}"} #{current} \u{2192} #{target} (#{action})"
|
|
96
|
-
|
|
94
|
+
prepare_switch(target, current)
|
|
95
|
+
cmd = ["npx", "#{PKG}@#{target}", "install", "--reinstall", "--ledger-action", action, *harness_flags]
|
|
97
96
|
puts " $ #{cmd.join(" ")}"
|
|
98
97
|
system(*cmd) ? 0 : 1
|
|
99
98
|
end
|
|
100
99
|
|
|
100
|
+
# Before handing off to an older package (a downgrade), strip Plastic's own current hook
|
|
101
|
+
# registrations from every installed harness (intent 310, cut-inventory R2). The older
|
|
102
|
+
# installer purges only the names its own registry knows, so an entry this version
|
|
103
|
+
# registered would otherwise survive while the older manifest diff deletes its launcher,
|
|
104
|
+
# and the older doctor would then report an unowned plastic- command. The older installer
|
|
105
|
+
# registers its own set fresh on the cleaned file. Returns the paths it stripped.
|
|
106
|
+
def prepare_switch(target, current)
|
|
107
|
+
return [] unless action_for(target, current) == "downgrade"
|
|
108
|
+
|
|
109
|
+
stripped = []
|
|
110
|
+
installed_agents.each do |key|
|
|
111
|
+
agent = agents.find { |a| a[:key] == key }
|
|
112
|
+
next unless agent
|
|
113
|
+
|
|
114
|
+
case key
|
|
115
|
+
when "claude"
|
|
116
|
+
path = File.join(agent[:dir], "settings.json")
|
|
117
|
+
if File.exist?(path)
|
|
118
|
+
remove_claude_hooks(path)
|
|
119
|
+
stripped << path
|
|
120
|
+
end
|
|
121
|
+
when "codex"
|
|
122
|
+
path = agent[:home_dir] && File.join(agent[:home_dir], "hooks.json")
|
|
123
|
+
if path && File.exist?(path)
|
|
124
|
+
remove_codex_hooks(path)
|
|
125
|
+
stripped << path
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
stripped
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# The install flags for every harness with a per-agent record (intent 310); Claude alone
|
|
133
|
+
# when nothing is recorded, matching the first-install default.
|
|
134
|
+
def harness_flags
|
|
135
|
+
keys = installed_agents
|
|
136
|
+
keys = ["claude"] if keys.empty?
|
|
137
|
+
keys.map { |k| "--#{k}" }
|
|
138
|
+
end
|
|
139
|
+
|
|
101
140
|
def print_table(ledger, current)
|
|
102
141
|
puts "\nPlastic version history (versions.json — append-only):\n\n"
|
|
103
142
|
puts " %-22s %-10s %s" % ["version", "action", "at"]
|
package/scripts/scaffold-intent
CHANGED
|
@@ -2,46 +2,38 @@
|
|
|
2
2
|
# encoding: UTF-8
|
|
3
3
|
# frozen_string_literal: true
|
|
4
4
|
|
|
5
|
-
# scaffold-intent (intent 213
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
# text
|
|
5
|
+
# scaffold-intent (intent 213; one verb since intent 308) - write an intent's judgment
|
|
6
|
+
# documents from its record. `backfill` fills spec.md, plan.md, actions/ACTION_1.md, and
|
|
7
|
+
# outcome.md wherever the file is missing or still the scaffold placeholder, from the
|
|
8
|
+
# intent file, checklist.md, and the diff (BackfillIntent). Every section it writes is a
|
|
9
|
+
# verbatim copy or a mechanical rendering of an already-committed artifact; a section with
|
|
10
|
+
# no source keeps the template's own stub text. A hand-written file is never touched, and
|
|
11
|
+
# there is no force flag: real content is edited by hand or not at all.
|
|
11
12
|
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
# [--test-summary <path>] (outcome subcommand only)
|
|
13
|
+
# `scripts/end-intent` runs the same writer at every close, so this CLI exists for the
|
|
14
|
+
# doctor fix hint (backfilled_complete) and for filling an intent by hand before its close.
|
|
15
15
|
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
# flags exist: --store, --id, --force, and --test-summary (outcome only) are the whole
|
|
20
|
-
# shipped surface (D5, a one-way door).
|
|
16
|
+
# Usage:
|
|
17
|
+
# scaffold-intent backfill --store <path> --id <intent_id> --disposition delivered|abandoned
|
|
18
|
+
# [--summary <text>]
|
|
21
19
|
#
|
|
22
20
|
# Exit codes:
|
|
23
|
-
# 0
|
|
21
|
+
# 0 written, or nothing left to write
|
|
24
22
|
# 1 usage or path-resolution failure
|
|
25
|
-
#
|
|
26
|
-
# 3 a prerequisite committed artifact is missing or still sentinel
|
|
27
|
-
#
|
|
28
|
-
# The actions/ rule (intent 133a, D11): scaffold-intent does not scaffold actions/ in any
|
|
29
|
-
# form. Actions require judgment and stay entirely with the planner. No subcommand
|
|
30
|
-
# creates the directory, writes a .gitkeep, or writes a sentinel-only ACTION_*.md.
|
|
23
|
+
# 3 a source is unusable (the intent file is missing)
|
|
31
24
|
#
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
# presence). This does NOT purge a bridge:
|
|
36
|
-
# Bridge.purge_done_bridges keys on INDEX Active status plus lock presence, never on the
|
|
37
|
-
# derived stage.
|
|
25
|
+
# The spec, checklist, and outcome subcommands, --force, and --test-summary were removed
|
|
26
|
+
# in 2.0 (intent 308): the backfill writer covers their two derivable targets and the
|
|
27
|
+
# outcome diffstat, and nothing else called them.
|
|
38
28
|
|
|
39
29
|
require_relative "lib/scaffold_intent"
|
|
30
|
+
require_relative "lib/backfill_intent"
|
|
40
31
|
|
|
41
|
-
SUBCOMMANDS = %w[
|
|
32
|
+
SUBCOMMANDS = %w[backfill].freeze
|
|
33
|
+
DISPOSITIONS = %w[delivered abandoned].freeze
|
|
42
34
|
|
|
43
35
|
def parse_args(argv)
|
|
44
|
-
opts = { subcommand: nil, store: nil, id: nil,
|
|
36
|
+
opts = { subcommand: nil, store: nil, id: nil, disposition: nil, summary: nil }
|
|
45
37
|
args = argv.dup
|
|
46
38
|
opts[:subcommand] = args.shift
|
|
47
39
|
|
|
@@ -49,10 +41,10 @@ def parse_args(argv)
|
|
|
49
41
|
while i < args.length
|
|
50
42
|
arg = args[i]
|
|
51
43
|
case arg
|
|
52
|
-
when "--store"
|
|
53
|
-
when "--id"
|
|
54
|
-
when "--
|
|
55
|
-
when "--
|
|
44
|
+
when "--store" then opts[:store] = args[i += 1]
|
|
45
|
+
when "--id" then opts[:id] = args[i += 1]
|
|
46
|
+
when "--disposition" then opts[:disposition] = args[i += 1]
|
|
47
|
+
when "--summary" then opts[:summary] = args[i += 1]
|
|
56
48
|
else
|
|
57
49
|
usage_abort("unknown argument #{arg.inspect}")
|
|
58
50
|
end
|
|
@@ -62,8 +54,8 @@ def parse_args(argv)
|
|
|
62
54
|
end
|
|
63
55
|
|
|
64
56
|
def usage
|
|
65
|
-
"usage: scaffold-intent
|
|
66
|
-
"
|
|
57
|
+
"usage: scaffold-intent backfill --store <store_path> --id <intent_id> " \
|
|
58
|
+
"--disposition delivered|abandoned [--summary <text>]"
|
|
67
59
|
end
|
|
68
60
|
|
|
69
61
|
def usage_abort(message)
|
|
@@ -75,11 +67,11 @@ end
|
|
|
75
67
|
def main(argv)
|
|
76
68
|
opts = parse_args(argv)
|
|
77
69
|
|
|
78
|
-
usage_abort("subcommand must be
|
|
70
|
+
usage_abort("subcommand must be #{SUBCOMMANDS.join('|')}") unless SUBCOMMANDS.include?(opts[:subcommand])
|
|
79
71
|
usage_abort("--store is required") if opts[:store].nil? || opts[:store].empty?
|
|
80
72
|
usage_abort("--id is required") if opts[:id].nil? || opts[:id].empty?
|
|
81
|
-
|
|
82
|
-
usage_abort("--
|
|
73
|
+
unless DISPOSITIONS.include?(opts[:disposition])
|
|
74
|
+
usage_abort("--disposition must be one of #{DISPOSITIONS.join('|')}")
|
|
83
75
|
end
|
|
84
76
|
|
|
85
77
|
store = ScaffoldIntent.expand(opts[:store])
|
|
@@ -88,33 +80,15 @@ def main(argv)
|
|
|
88
80
|
intent_dir, err = ScaffoldIntent.resolve_intent_dir(store, opts[:id])
|
|
89
81
|
usage_abort(err) if intent_dir.nil?
|
|
90
82
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
test_summary_path = ScaffoldIntent.expand(opts[:test_summary])
|
|
94
|
-
usage_abort("--test-summary path does not exist: #{test_summary_path}") unless File.exist?(test_summary_path)
|
|
95
|
-
end
|
|
83
|
+
result = BackfillIntent.run(intent_dir: intent_dir, store: store, id: opts[:id],
|
|
84
|
+
disposition: opts[:disposition], summary: opts[:summary])
|
|
96
85
|
|
|
97
|
-
result
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
when "checklist"
|
|
102
|
-
ScaffoldIntent.scaffold_checklist(intent_dir: intent_dir, force: opts[:force])
|
|
103
|
-
when "outcome"
|
|
104
|
-
ScaffoldIntent.scaffold_outcome(intent_dir: intent_dir, force: opts[:force], store: store,
|
|
105
|
-
id: opts[:id], test_summary: test_summary_path)
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
if result[:message]
|
|
109
|
-
if result[:code] == 0
|
|
110
|
-
puts result[:message]
|
|
111
|
-
else
|
|
112
|
-
warn "scaffold-intent: #{result[:message]}"
|
|
113
|
-
end
|
|
114
|
-
end
|
|
115
|
-
puts result[:path] if result[:path]
|
|
86
|
+
result[:written].each { |rel| puts "scaffold-intent: backfilled #{File.join(intent_dir, rel)}" }
|
|
87
|
+
result[:skipped].each { |rel| puts "scaffold-intent: kept #{rel} (real content)" }
|
|
88
|
+
result[:notes].each { |note| warn "scaffold-intent: #{note}" }
|
|
89
|
+
puts "scaffold-intent: nothing to backfill" if result[:written].empty? && result[:notes].empty?
|
|
116
90
|
|
|
117
|
-
exit
|
|
91
|
+
exit(result[:notes].any? { |n| n.include?("intent file is missing") } ? 3 : 0)
|
|
118
92
|
end
|
|
119
93
|
|
|
120
94
|
main(ARGV) if $PROGRAM_NAME == __FILE__
|