@zalom/plastic 2.0.0-alpha.22 → 2.0.0-alpha.24
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 +6 -5
- package/agents/plastic-enforcer.md +6 -3
- package/agents/plastic-executor.md +4 -0
- package/agents/plastic-node-research.md +28 -0
- package/agents/plastic-node-verify.md +27 -0
- package/agents/plastic-node-work.md +32 -0
- package/bin/lib/context_budget.rb +1 -1
- package/hooks/hooks.json +12 -0
- package/hooks/statusline +28 -0
- package/hooks/stop +5 -0
- package/package.json +1 -1
- package/scripts/dashboard.rb +1 -1
- package/scripts/doctor.rb +80 -6
- package/scripts/end-intent +22 -19
- package/scripts/exec-worktree +5 -5
- package/scripts/graph-measure +249 -0
- package/scripts/hook-call-budget +6 -6
- package/scripts/hook-capture +16 -15
- package/scripts/hook-record +21 -14
- package/scripts/hook-savepoint +26 -3
- package/scripts/hook-session-start +49 -10
- package/scripts/hook-stop +57 -0
- package/scripts/lib/active_delivery.rb +105 -0
- package/scripts/lib/agent_models.rb +10 -1
- package/scripts/lib/arm.rb +41 -102
- package/scripts/lib/codex_adapter.rb +197 -0
- package/scripts/lib/{packet_wrapper.rb → data_boundary.rb} +7 -7
- package/scripts/lib/day_summary.rb +19 -11
- package/scripts/lib/doctor_core.rb +8 -3
- package/scripts/lib/doctor_session_ledger.rb +3 -52
- package/scripts/lib/engine_permissions.rb +88 -0
- package/scripts/lib/exec_worktree.rb +24 -21
- package/scripts/lib/graph_edges.rb +4 -4
- package/scripts/lib/graph_file.rb +4 -4
- package/scripts/lib/graph_measure.rb +645 -0
- package/scripts/lib/graph_measure_budget.rb +409 -0
- package/scripts/lib/graph_measure_cohorts.rb +487 -0
- package/scripts/lib/graph_measure_models.rb +411 -0
- package/scripts/lib/graph_measure_report.rb +532 -0
- package/scripts/lib/graph_tree.rb +2 -2
- package/scripts/lib/handoff.rb +40 -13
- package/scripts/lib/harness_adapter.rb +184 -0
- package/scripts/lib/hook_registry.rb +13 -1
- package/scripts/lib/hook_replay.rb +23 -5
- package/scripts/lib/index_entry.rb +53 -0
- package/scripts/lib/index_projection.rb +1 -1
- package/scripts/lib/insights.rb +1 -1
- package/scripts/lib/installer_core.rb +228 -9
- package/scripts/lib/intent_screen.rb +1 -1
- package/scripts/lib/intent_validator.rb +2 -2
- package/scripts/lib/lock.rb +8 -9
- package/scripts/lib/meter_watch.rb +15 -9
- package/scripts/lib/node_file.rb +3 -3
- package/scripts/lib/{node_packet.rb → node_input.rb} +49 -49
- package/scripts/lib/node_input_compatibility.rb +62 -0
- package/scripts/lib/node_ledger.rb +11 -2
- package/scripts/lib/node_progress.rb +153 -0
- package/scripts/lib/outcome_report.rb +2 -2
- package/scripts/lib/project_config.rb +45 -0
- package/scripts/lib/ready_set.rb +1 -1
- package/scripts/lib/report_screen.rb +10 -6
- package/scripts/lib/roadmap_graph.rb +1 -1
- package/scripts/lib/roadmap_queue.rb +1 -1
- package/scripts/lib/roadmap_render.rb +1 -1
- package/scripts/lib/roadmap_savepoint.rb +1 -1
- package/scripts/lib/runner_absorb.rb +36 -10
- package/scripts/lib/runner_dispatch.rb +66 -51
- package/scripts/lib/runner_sweep.rb +4 -4
- package/scripts/lib/runner_until_empty.rb +252 -0
- package/scripts/lib/runner_watch.rb +389 -0
- package/scripts/lib/savepoint.rb +9 -10
- package/scripts/lib/scaffold_intent.rb +6 -3
- package/scripts/lib/session_close.rb +30 -28
- package/scripts/lib/session_git.rb +22 -16
- package/scripts/lib/session_ledger.rb +44 -4
- package/scripts/lib/stop_gate.rb +95 -0
- package/scripts/lib/verify_intent.rb +2 -2
- package/scripts/lib/work_graph_validator.rb +6 -6
- package/scripts/lib/worktree.rb +24 -24
- package/scripts/lib/worktree_sweep.rb +3 -3
- package/scripts/new-intent +1 -1
- package/scripts/{node-packet → node-input} +15 -15
- package/scripts/node-run +224 -0
- package/scripts/plastic-lock +33 -32
- package/scripts/read-config +6 -0
- package/scripts/runner +203 -19
- package/scripts/verify-intent +1 -1
- package/skills/auto/SKILL.md +8 -8
- package/skills/auto/references/end-tail.md +9 -11
- package/skills/conventions/references/completion-and-done.md +9 -10
- package/skills/conventions/references/knowledge-graph.md +9 -0
- package/skills/conventions/references/locks-and-worktrees.md +10 -12
- package/skills/direct/SKILL.md +2 -2
- package/skills/doctor/SKILL.md +4 -4
- package/skills/intent-creating/evals/evals.json +1 -1
- package/skills/intent-executing/SKILL.md +7 -1
- package/skills/releasing/SKILL.md +2 -2
- package/skills/releasing/references/promotion-and-tagging.md +1 -1
- package/skills/releasing/references/release-lines.md +1 -1
- package/skills/tutorial/references/track-2-auto.md +1 -1
- package/templates/index.md +1 -1
- package/templates/project.yml +1 -1
- package/scripts/lib/bridge.rb +0 -116
package/scripts/plastic-lock
CHANGED
|
@@ -19,12 +19,11 @@
|
|
|
19
19
|
# corrupt lock with the resolving doctor hint
|
|
20
20
|
# who compact, read-only durable owner/activity view
|
|
21
21
|
# status report the lock file, its freshness, the derived worktree,
|
|
22
|
-
# whether this session
|
|
23
|
-
# live per-artifact claims
|
|
22
|
+
# whether this session is the one delivering the intent, and
|
|
23
|
+
# any live per-artifact claims
|
|
24
24
|
# fix idempotent repair of the lock for the current session; never
|
|
25
25
|
# touches a fresh foreign lock
|
|
26
|
-
# release owner clears the lock
|
|
27
|
-
# ledger (End tail / abandoning a boarding)
|
|
26
|
+
# release owner clears the lock (End tail / abandoning a boarding)
|
|
28
27
|
# reclaim explicit takeover of a stale lock, audited in savepoint.md
|
|
29
28
|
# delegate owner registers a subagent session under the lock (D4)
|
|
30
29
|
# claim acquire the named --artifact's claim token (intent 111);
|
|
@@ -32,18 +31,18 @@
|
|
|
32
31
|
# including this same session; takes over a stale claim
|
|
33
32
|
# release-claim free the named --artifact's claim token
|
|
34
33
|
#
|
|
35
|
-
# Without --intent-dir the intent is resolved from this session's
|
|
36
|
-
# (
|
|
37
|
-
#
|
|
38
|
-
#
|
|
39
|
-
#
|
|
34
|
+
# Without --intent-dir the intent is resolved from this session's held
|
|
35
|
+
# delivery lock (ActiveDelivery.resolve over the global store and the
|
|
36
|
+
# configured project roots): exactly one fresh lock this session owns or
|
|
37
|
+
# delegates for. Session resolution no longer reads any per-session state
|
|
38
|
+
# file; that path was retired in 2.0 (intent 307) and 344 G11.
|
|
39
|
+
# Exit 0 on success/report; exit 1 when the verb is blocked.
|
|
40
40
|
|
|
41
41
|
require "json"
|
|
42
42
|
require "yaml"
|
|
43
|
-
require_relative "lib/bridge"
|
|
44
43
|
require_relative "lib/lock"
|
|
45
44
|
require_relative "lib/arm"
|
|
46
|
-
require_relative "lib/
|
|
45
|
+
require_relative "lib/active_delivery"
|
|
47
46
|
|
|
48
47
|
VERBS = %w[arm status who fix release reclaim delegate claim release-claim].freeze
|
|
49
48
|
|
|
@@ -88,16 +87,7 @@ hint_harness = harness || "claude"
|
|
|
88
87
|
plastic_home_env = ENV["PLASTIC_HOME"].to_s.strip
|
|
89
88
|
home = plastic_home_env.empty? ? Dir.home : File.dirname(File.expand_path(plastic_home_env))
|
|
90
89
|
|
|
91
|
-
|
|
92
|
-
# the working directory (projects.yml, longest path match), then the global one.
|
|
93
|
-
def candidate_stores(home, cwd)
|
|
94
|
-
plastic_home = File.join(home, ".plastic")
|
|
95
|
-
stores = []
|
|
96
|
-
slug = SessionLedger.project_slug(cwd, plastic_home: plastic_home)
|
|
97
|
-
stores << File.join(plastic_home, "projects", slug, "store") if slug && slug != "global"
|
|
98
|
-
stores << File.join(plastic_home, "store")
|
|
99
|
-
stores
|
|
100
|
-
end
|
|
90
|
+
plastic_home = File.join(home, ".plastic")
|
|
101
91
|
|
|
102
92
|
dir = opts[:dir]
|
|
103
93
|
if dir.nil? || dir.strip.empty?
|
|
@@ -105,11 +95,21 @@ if dir.nil? || dir.strip.empty?
|
|
|
105
95
|
warn "plastic-lock: #{verb} needs --intent-dir <intent dir>"
|
|
106
96
|
exit 1
|
|
107
97
|
end
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
98
|
+
roots = ActiveDelivery.project_roots(plastic_home)
|
|
99
|
+
global_store = File.join(plastic_home, "store")
|
|
100
|
+
dir = ActiveDelivery.resolve(global_store: global_store, project_roots: roots, session: session)
|
|
101
|
+
if dir.nil?
|
|
102
|
+
candidates = ActiveDelivery.candidate_intent_dirs(global_store: global_store, project_roots: roots).uniq
|
|
103
|
+
held = candidates.select do |candidate|
|
|
104
|
+
Lock.fresh?(candidate) && Lock.holds?(candidate, session: session)
|
|
105
|
+
end
|
|
106
|
+
if held.size > 1
|
|
107
|
+
warn "plastic-lock: more than one held delivery lock names this session; pass --intent-dir <intent dir>"
|
|
108
|
+
else
|
|
109
|
+
warn "plastic-lock: no intent resolved; pass --intent-dir <intent dir>"
|
|
110
|
+
end
|
|
111
|
+
exit 1
|
|
112
|
+
end
|
|
113
113
|
end
|
|
114
114
|
dir = File.expand_path(dir)
|
|
115
115
|
|
|
@@ -144,7 +144,7 @@ if verb == "who"
|
|
|
144
144
|
exit 0
|
|
145
145
|
end
|
|
146
146
|
|
|
147
|
-
intent_id =
|
|
147
|
+
intent_id = Arm.intent_id_for(dir)
|
|
148
148
|
store = File.dirname(dir)
|
|
149
149
|
key = Arm.resolve_session(session, store: store, intent_id: intent_id)
|
|
150
150
|
|
|
@@ -163,7 +163,7 @@ when "arm"
|
|
|
163
163
|
puts JSON.pretty_generate(
|
|
164
164
|
"intent_dir" => dir, "session" => result[:session], "status" => result[:status].to_s,
|
|
165
165
|
"run_mode" => result[:lock] && result[:lock]["run_mode"],
|
|
166
|
-
"worktree" => wt
|
|
166
|
+
"worktree" => wt
|
|
167
167
|
)
|
|
168
168
|
when :held
|
|
169
169
|
warn "plastic-lock: delivery lock for intent #{intent_id} is held by session " \
|
|
@@ -188,7 +188,10 @@ when "arm"
|
|
|
188
188
|
end
|
|
189
189
|
when "status"
|
|
190
190
|
lock = Lock.read(dir)
|
|
191
|
-
|
|
191
|
+
resolved = ActiveDelivery.resolve(global_store: File.join(plastic_home, "store"),
|
|
192
|
+
project_roots: ActiveDelivery.project_roots(plastic_home),
|
|
193
|
+
session: key)
|
|
194
|
+
delivering = !resolved.nil? && File.expand_path(resolved) == File.expand_path(dir)
|
|
192
195
|
report = {
|
|
193
196
|
"intent_dir" => dir,
|
|
194
197
|
"session" => key,
|
|
@@ -196,8 +199,7 @@ when "status"
|
|
|
196
199
|
"lock_fresh" => Lock.fresh?(dir),
|
|
197
200
|
"lock_corrupt" => Lock.corrupt?(dir),
|
|
198
201
|
"worktree" => Arm.worktree_block(intent_dir: dir, home: home),
|
|
199
|
-
"
|
|
200
|
-
"pointer_names_intent" => pointer == intent_id,
|
|
202
|
+
"delivering" => delivering,
|
|
201
203
|
"claims" => Claim.claims_status(dir),
|
|
202
204
|
}
|
|
203
205
|
puts JSON.pretty_generate(report)
|
|
@@ -217,7 +219,6 @@ when "release"
|
|
|
217
219
|
warn "plastic-lock: not the owner; run plastic-lock status"
|
|
218
220
|
exit 1
|
|
219
221
|
end
|
|
220
|
-
Arm.reset_pointer(key, intent_id, home: home)
|
|
221
222
|
puts "released (#{result})"
|
|
222
223
|
when "reclaim"
|
|
223
224
|
status, lock_data = Lock.takeover(dir, session: key, harness: harness,
|
package/scripts/read-config
CHANGED
|
@@ -33,6 +33,12 @@ DEFAULTS = {
|
|
|
33
33
|
},
|
|
34
34
|
"architect" => {
|
|
35
35
|
"style" => nil
|
|
36
|
+
},
|
|
37
|
+
# Intent 340b (G7c, n4, D9): the Stop hook's runtime arm. Off until the
|
|
38
|
+
# owner's C32 ruling; read-config emits it as the string "false", which
|
|
39
|
+
# StopGate parses as a boolean itself rather than trusting.
|
|
40
|
+
"runner" => {
|
|
41
|
+
"stop_hook" => false
|
|
36
42
|
}
|
|
37
43
|
}.freeze
|
|
38
44
|
|
package/scripts/runner
CHANGED
|
@@ -2,24 +2,28 @@
|
|
|
2
2
|
# encoding: UTF-8
|
|
3
3
|
# frozen_string_literal: true
|
|
4
4
|
|
|
5
|
+
require "yaml"
|
|
6
|
+
require "stringio"
|
|
5
7
|
require_relative "lib/savepoint"
|
|
6
8
|
require_relative "lib/ready_set"
|
|
7
9
|
require_relative "lib/runner_core"
|
|
10
|
+
require_relative "lib/harness_adapter"
|
|
8
11
|
require_relative "lib/runner_proposals"
|
|
9
12
|
|
|
10
13
|
# runner - the one executable over the graph-ready loop's declared node graph
|
|
11
14
|
# (intent 340, G7, n1). A subcommand table: the public verbs (step, status,
|
|
12
15
|
# answer) are the only ones the skill body ever names; the internal verbs
|
|
13
|
-
# (ready, sweep, rewind) route and work so `step` can compose
|
|
14
|
-
# each can be tested alone, but stay out of the usage text (327
|
|
16
|
+
# (ready, sweep, rewind, until-empty) route and work so `step` can compose
|
|
17
|
+
# them and so each can be tested alone, but stay out of the usage text (327
|
|
18
|
+
# spec, 340b n7).
|
|
15
19
|
#
|
|
16
|
-
# `step`, `sweep`, `answer` and `
|
|
17
|
-
# deliver (RunnerDispatch and RunnerAbsorb for `step`;
|
|
18
|
-
# RunnerAnswer, RunnerRewind for the rest).
|
|
19
|
-
# its own verb branch: a verb whose module
|
|
20
|
-
# plainly and exits nonzero, and every other verb
|
|
21
|
-
# `status`, which an operator polls constantly across all
|
|
22
|
-
# intent's dispatches - keeps working.
|
|
20
|
+
# `step`, `sweep`, `answer`, `rewind` and `until-empty` route to modules
|
|
21
|
+
# later nodes deliver (RunnerDispatch and RunnerAbsorb for `step`;
|
|
22
|
+
# RunnerSweep, RunnerAnswer, RunnerRewind, RunnerUntilEmpty for the rest).
|
|
23
|
+
# Each is required LAZILY, inside its own verb branch: a verb whose module
|
|
24
|
+
# has not landed yet reports plainly and exits nonzero, and every other verb
|
|
25
|
+
# - most importantly `status`, which an operator polls constantly across all
|
|
26
|
+
# five of this intent's dispatches - keeps working.
|
|
23
27
|
#
|
|
24
28
|
# Usage:
|
|
25
29
|
# runner <step|status|answer> <intent_dir> [--node ID] [--answer TEXT]
|
|
@@ -34,7 +38,7 @@ module Runner
|
|
|
34
38
|
module_function
|
|
35
39
|
|
|
36
40
|
PUBLIC_VERBS = %w[step status answer].freeze
|
|
37
|
-
INTERNAL_VERBS = %w[ready sweep rewind].freeze
|
|
41
|
+
INTERNAL_VERBS = %w[ready sweep rewind until-empty watch].freeze
|
|
38
42
|
VERBS = (PUBLIC_VERBS + INTERNAL_VERBS).freeze
|
|
39
43
|
|
|
40
44
|
# verb -> [[ModuleName, lib_file], ...], required lazily inside the verb's
|
|
@@ -47,6 +51,8 @@ module Runner
|
|
|
47
51
|
"sweep" => [["RunnerSweep", "runner_sweep"]],
|
|
48
52
|
"answer" => [["RunnerAnswer", "runner_answer"]],
|
|
49
53
|
"rewind" => [["RunnerRewind", "runner_rewind"]],
|
|
54
|
+
"until-empty" => [["RunnerUntilEmpty", "runner_until_empty"]],
|
|
55
|
+
"watch" => [["RunnerWatch", "runner_watch"]],
|
|
50
56
|
}.freeze
|
|
51
57
|
|
|
52
58
|
def usage
|
|
@@ -77,11 +83,21 @@ module Runner
|
|
|
77
83
|
# a mistyped `--session` used to vanish with no effect and the runner
|
|
78
84
|
# simply acted as whichever session `CLAUDE_CODE_SESSION_ID` names.
|
|
79
85
|
KNOWN_FLAGS = {
|
|
80
|
-
"step" => %w[--return --allow-core-drift],
|
|
86
|
+
"step" => %w[--return --allow-core-drift --harness],
|
|
81
87
|
"answer" => %w[--node --answer],
|
|
82
88
|
"rewind" => %w[--node --confirm],
|
|
89
|
+
"until-empty" => %w[--harness],
|
|
90
|
+
"watch" => %w[--dispatch --harness --install-timer --home],
|
|
83
91
|
}.freeze
|
|
84
92
|
|
|
93
|
+
# The explicit YAML document-end marker (intent 340b, G7c, n1, row 1.28)
|
|
94
|
+
# separating the dispatch plan `step` has always printed from the
|
|
95
|
+
# harness-rendered block underneath it: stdout has never been one YAML
|
|
96
|
+
# document (absorb/reclaim/extend/reap lines print before the plan,
|
|
97
|
+
# decision/park lines after it), so a reader splits on this marker rather
|
|
98
|
+
# than guessing where the plan ends.
|
|
99
|
+
DOCUMENT_BOUNDARY = "...".freeze
|
|
100
|
+
|
|
85
101
|
# unrecognized_flag(verb, args) -> the first token in `args` that looks
|
|
86
102
|
# like a flag (`--...`) and is not in `verb`'s own KNOWN_FLAGS, or nil. A
|
|
87
103
|
# verb absent from KNOWN_FLAGS (status, ready, sweep) accepts none.
|
|
@@ -203,6 +219,10 @@ module Runner
|
|
|
203
219
|
run_answer(intent_dir, args)
|
|
204
220
|
when "rewind"
|
|
205
221
|
run_rewind(intent_dir, args)
|
|
222
|
+
when "until-empty"
|
|
223
|
+
run_until_empty(intent_dir, args)
|
|
224
|
+
when "watch"
|
|
225
|
+
run_watch(intent_dir, args)
|
|
206
226
|
else
|
|
207
227
|
warn "runner: #{verb}'s module loaded but no dispatcher is wired up yet"
|
|
208
228
|
exit 3
|
|
@@ -284,14 +304,156 @@ module Runner
|
|
|
284
304
|
exit 0
|
|
285
305
|
end
|
|
286
306
|
|
|
287
|
-
#
|
|
288
|
-
#
|
|
289
|
-
#
|
|
290
|
-
#
|
|
291
|
-
#
|
|
292
|
-
#
|
|
293
|
-
#
|
|
307
|
+
# run_until_empty (intent 340b, G7c, n7): the Codex loop. Internal, like
|
|
308
|
+
# `rewind` (row 7.13) - `RunnerUntilEmpty.run` composes `step` and
|
|
309
|
+
# `node-run` itself, since Codex has no session on the other end to make
|
|
310
|
+
# the subagent calls `step` only ever prints a plan for. `--harness`
|
|
311
|
+
# threads into every turn the loop runs (row 7.5), never re-resolved per
|
|
312
|
+
# iteration from a literal. The lock check here is the same defence in
|
|
313
|
+
# depth `run_step_body` carries (B4): `RunnerUntilEmpty.step_once` refuses
|
|
314
|
+
# `lock_not_held` on its own first call regardless, so this exits before
|
|
315
|
+
# even constructing the loop's own state.
|
|
316
|
+
def run_until_empty(intent_dir, args)
|
|
317
|
+
harness_override = opt(args, "--harness")
|
|
318
|
+
context = RunnerCore.context(intent_dir: intent_dir, env: ENV["CLAUDE_CODE_SESSION_ID"])
|
|
319
|
+
|
|
320
|
+
unless context.session
|
|
321
|
+
warn "runner: until-empty refused (lock_not_held)"
|
|
322
|
+
exit 1
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
result = RunnerUntilEmpty.run(context, harness: harness_override)
|
|
326
|
+
|
|
327
|
+
case result[:status]
|
|
328
|
+
when "complete", "stalled", "needs_decision", "iteration_cap"
|
|
329
|
+
exit 0
|
|
330
|
+
else
|
|
331
|
+
exit 1
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
# run_watch (intent 340a, G7b, n2): one tick over disk truth. Internal,
|
|
336
|
+
# like until-empty and rewind - the timer that carries it lives outside
|
|
337
|
+
# this process (a local `/loop` on Claude Code, a launchd job on Codex,
|
|
338
|
+
# n3/n4). `--dispatch` is unattended start (graph.md D7): refused here,
|
|
339
|
+
# before RunnerWatch.tick ever takes the tick lock or writes anything,
|
|
340
|
+
# when the resolved harness's `HarnessAdapter.unattended_start?` is
|
|
341
|
+
# false, so a refusal never leaves a `watch.record` line behind that
|
|
342
|
+
# looks like a completed tick.
|
|
343
|
+
# --install-timer (intent 340a, G7b, n3, graph.md D9): the Codex carrier.
|
|
344
|
+
# Writes the plist and prints the path plus the `launchctl load` command;
|
|
345
|
+
# never runs `launchctl` itself, so the owner stays in control of when the
|
|
346
|
+
# job actually loads. `--home` defaults to the OS home, as
|
|
347
|
+
# `scripts/meter-watch --install-timer` already does.
|
|
348
|
+
def run_watch(intent_dir, args)
|
|
349
|
+
dispatch = args.include?("--dispatch")
|
|
350
|
+
harness_override = opt(args, "--harness")
|
|
351
|
+
context = RunnerCore.context(intent_dir: intent_dir, env: ENV["CLAUDE_CODE_SESSION_ID"])
|
|
352
|
+
agent_config = load_agent_config(context.plastic_home)
|
|
353
|
+
harness = HarnessAdapter.resolve_key(config: agent_config, override: harness_override)
|
|
354
|
+
|
|
355
|
+
if args.include?("--install-timer")
|
|
356
|
+
home = opt(args, "--home") || Dir.home
|
|
357
|
+
plist_path = RunnerWatch.install_timer(context, home: home, harness_key: harness)
|
|
358
|
+
puts "plist: #{plist_path}"
|
|
359
|
+
puts "launchctl load #{plist_path}"
|
|
360
|
+
exit 0
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
if dispatch && !HarnessAdapter.unattended_start?(harness)
|
|
364
|
+
warn HarnessAdapter::UNATTENDED_START_SENTENCE
|
|
365
|
+
exit 1
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
result = RunnerWatch.tick(context, dispatch: dispatch, harness: harness)
|
|
369
|
+
|
|
370
|
+
puts "class: #{result[:class]}"
|
|
371
|
+
result[:blockers].each { |b| puts "blocked: #{b}" }
|
|
372
|
+
puts "ready: #{result[:ready].empty? ? '(none)' : result[:ready].join(', ')}"
|
|
373
|
+
puts "dispatched: #{result[:dispatched].empty? ? '(none)' : result[:dispatched].join(', ')}"
|
|
374
|
+
exit 0
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
# run_step (intent 340, G7, n5): one turn of the loop. The real work lives
|
|
378
|
+
# in #run_step_body below; this wrapper (intent 340b, G7c, n1, rows
|
|
379
|
+
# 1.21-1.25 and 1.29-1.31) exists ONLY to capture every byte `step` prints
|
|
380
|
+
# - stdout and stderr both, refusals included - and persist it to
|
|
381
|
+
# `<intent_dir>/runner-step.last` before the process actually exits, so a
|
|
382
|
+
# PreCompact hand-off (n3) always has something to render, even after one
|
|
383
|
+
# of the three pre-plan refusals that used to warn to stderr alone.
|
|
384
|
+
#
|
|
385
|
+
# `run_step_body` still calls Kernel#exit(N) at every one of its own exit
|
|
386
|
+
# points, exactly as before this node; wrapping it in a rescue for
|
|
387
|
+
# SystemExit lets this method capture the intended status, persist the
|
|
388
|
+
# file, replay the captured output onto the REAL stdout/stderr (so a
|
|
389
|
+
# subprocess caller like Open3.capture3 sees byte-identical output to
|
|
390
|
+
# before this node), and only then perform the one real process exit.
|
|
294
391
|
def run_step(intent_dir, args)
|
|
392
|
+
out_buf = StringIO.new
|
|
393
|
+
err_buf = StringIO.new
|
|
394
|
+
real_stdout = $stdout
|
|
395
|
+
real_stderr = $stderr
|
|
396
|
+
exit_code = 0
|
|
397
|
+
begin
|
|
398
|
+
$stdout = out_buf
|
|
399
|
+
$stderr = err_buf
|
|
400
|
+
run_step_body(intent_dir, args)
|
|
401
|
+
rescue SystemExit => e
|
|
402
|
+
exit_code = e.status || 1
|
|
403
|
+
ensure
|
|
404
|
+
$stdout = real_stdout
|
|
405
|
+
$stderr = real_stderr
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
real_stdout.print(out_buf.string)
|
|
409
|
+
real_stderr.print(err_buf.string)
|
|
410
|
+
persist_runner_step_last(intent_dir, out_buf.string, err_buf.string)
|
|
411
|
+
|
|
412
|
+
exit(exit_code)
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
# Row 1.24: lives beside savepoint.md, never inside attempts/ (which the
|
|
416
|
+
# reaper treats as attempt evidence). Row 1.32: registers itself in the
|
|
417
|
+
# store's own top-level `.gitignore` (Worktree.ensure_gitignored's own
|
|
418
|
+
# idempotent, non-raising append) the same way `*.lock` already is, so a
|
|
419
|
+
# file rewritten on every step never dirties the store tree. Row 1.25:
|
|
420
|
+
# wrapped in its own rescue - an unwritable intent directory must never
|
|
421
|
+
# crash a step that has already written its transitions.
|
|
422
|
+
def persist_runner_step_last(intent_dir, stdout_text, stderr_text)
|
|
423
|
+
home_root = Arm.home_for(intent_dir)
|
|
424
|
+
Worktree.ensure_gitignored(File.join(home_root, ".plastic"), "runner-step.last")
|
|
425
|
+
|
|
426
|
+
path = File.join(intent_dir, "runner-step.last")
|
|
427
|
+
File.write(path, "#{stdout_text}#{stderr_text}")
|
|
428
|
+
rescue StandardError => e
|
|
429
|
+
warn "runner: could not persist runner-step.last: #{e.message}"
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
# Intent 340b, G7c, n1, row 1.1: the config `HarnessAdapter.resolve_key`
|
|
433
|
+
# reads `agent.type` from. `context.plastic_home` is derived from the
|
|
434
|
+
# intent_dir's OWN ancestry (Arm.home_for -> Worktree.home_from_store), so
|
|
435
|
+
# a sandboxed test fixture never reaches the real ~/.plastic/config.yml
|
|
436
|
+
# (Arm.home_for's own docstring: "a sandboxed store never resolves to the
|
|
437
|
+
# real Dir.home") - this stays hermetic for free, the same way every other
|
|
438
|
+
# `context.plastic_home` read in this file already does. A missing or
|
|
439
|
+
# unparseable config.yml reads as {}, never raises.
|
|
440
|
+
def load_agent_config(plastic_home)
|
|
441
|
+
path = File.join(plastic_home.to_s, "config.yml")
|
|
442
|
+
return {} unless File.exist?(path)
|
|
443
|
+
|
|
444
|
+
YAML.safe_load(File.read(path)) || {}
|
|
445
|
+
rescue StandardError
|
|
446
|
+
{}
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
# run_step_body (intent 340, G7, n5): one turn of the loop, in the fixed
|
|
450
|
+
# order 327 D14 and RunnerSweep's own docstring name - abort on a stuck
|
|
451
|
+
# merge, the delivery-lease heartbeat, absorb every `--return NODE=PATH`
|
|
452
|
+
# this call carries, THEN reclaim (so a node this very call just absorbed
|
|
453
|
+
# is never reclaimed out from under its own return, matrix row 2.19/2.20) -
|
|
454
|
+
# and only then dispatch whatever is left ready. Never spawns an agent (327
|
|
455
|
+
# D42): the session makes every subagent call from the plan this prints.
|
|
456
|
+
def run_step_body(intent_dir, args)
|
|
295
457
|
context = RunnerCore.context(intent_dir: intent_dir, env: ENV["CLAUDE_CODE_SESSION_ID"])
|
|
296
458
|
|
|
297
459
|
# v1 minor 3/row 11.18: RunnerSweep.abort_if_merging already prints the
|
|
@@ -322,6 +484,12 @@ module Runner
|
|
|
322
484
|
# never defaulted true.
|
|
323
485
|
allow_core_drift = args.include?("--allow-core-drift")
|
|
324
486
|
|
|
487
|
+
# Intent 340b, G7c, n1, row 1.2/1.4: `--harness KEY` overrides config
|
|
488
|
+
# `agent.type` for this one call - how one machine drives both adapters
|
|
489
|
+
# without editing config. `nil` when absent, so HarnessAdapter falls
|
|
490
|
+
# through to config's own value untouched.
|
|
491
|
+
harness_override = opt(args, "--harness")
|
|
492
|
+
|
|
325
493
|
returns = {}
|
|
326
494
|
opt_all(args, "--return").each do |pair|
|
|
327
495
|
node, path = pair.to_s.split("=", 2)
|
|
@@ -351,7 +519,8 @@ module Runner
|
|
|
351
519
|
reaped = NodeWorktree.reap(context)
|
|
352
520
|
reaped[:removed].each { |r| puts "reaped #{r[:node]} worktree" }
|
|
353
521
|
|
|
354
|
-
|
|
522
|
+
agent_config = load_agent_config(context.plastic_home)
|
|
523
|
+
result = RunnerDispatch.dispatch(context, config: agent_config, harness: harness_override)
|
|
355
524
|
unless result[:ok]
|
|
356
525
|
warn "runner: step refused (#{result[:reason]}): #{Array(result[:errors]).join('; ')}"
|
|
357
526
|
warn result[:rearm_command] if result[:rearm_command]
|
|
@@ -374,6 +543,21 @@ module Runner
|
|
|
374
543
|
result[:blockers].each { |b| puts "blocked: #{b}" }
|
|
375
544
|
end
|
|
376
545
|
|
|
546
|
+
# Intent 340b, G7c, n1, rows 1.6/1.13/1.14/1.27/1.28: the YAML plan keeps
|
|
547
|
+
# printing exactly as it did before this node (327's contract) - this is
|
|
548
|
+
# the rendered block UNDERNEATH it, for the harness `result[:harness]`
|
|
549
|
+
# resolved (config's `agent.type`, or this call's own `--harness`
|
|
550
|
+
# override), separated by an explicit YAML document-end marker so a
|
|
551
|
+
# reader splits on that marker rather than a heuristic. `render` returns
|
|
552
|
+
# nil for an empty dispatch list (row 1.14), so nothing extra ever
|
|
553
|
+
# prints when this step dispatched nothing.
|
|
554
|
+
block = HarnessAdapter.render(result[:dispatched], harness: result[:harness],
|
|
555
|
+
return_contract: RunnerDispatch::RETURN_CONTRACT)
|
|
556
|
+
if block
|
|
557
|
+
puts DOCUMENT_BOUNDARY
|
|
558
|
+
puts block
|
|
559
|
+
end
|
|
560
|
+
|
|
377
561
|
# M11/row 10.14: a `needs_decision` stop must print even when THIS SAME
|
|
378
562
|
# step also dispatched other nodes ahead of the decision node in ranked
|
|
379
563
|
# order - `result[:status]` reads "dispatched" in that case (dispatched
|
package/scripts/verify-intent
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
#
|
|
12
12
|
# --base overrides the auto-detected diff base (default: the merge base with the repo's
|
|
13
13
|
# detected default branch) for repos where detection is ambiguous. --suite is an optional
|
|
14
|
-
# caller-supplied command to run (in the repo directory) and
|
|
14
|
+
# caller-supplied command to run (in the repo directory) and merge into the verdict. No
|
|
15
15
|
# other flags exist.
|
|
16
16
|
#
|
|
17
17
|
# Checks, all run every invocation (the fourth only when --suite is given):
|
package/skills/auto/SKILL.md
CHANGED
|
@@ -46,10 +46,10 @@ QMD-first (when available): when the user describes the work instead of naming a
|
|
|
46
46
|
## Take the intent (do this FIRST)
|
|
47
47
|
|
|
48
48
|
Immediately after selecting the intent, take it for this session. One verb acquires the durable
|
|
49
|
-
`delivery.lock` in the intent directory (stamped `run_mode: auto`)
|
|
50
|
-
at `<repo>/.claude/worktrees/{id}--{slug}` on branch `plastic/{id}--{slug}
|
|
51
|
-
session
|
|
52
|
-
|
|
49
|
+
`delivery.lock` in the intent directory (stamped `run_mode: auto`) and provisions the code worktree
|
|
50
|
+
at `<repo>/.claude/worktrees/{id}--{slug}` on branch `plastic/{id}--{slug}`; the delivery lock
|
|
51
|
+
names this session as the one delivering the intent, so the record hook writes savepoint lines
|
|
52
|
+
and heartbeats for it instead of the day ledger:
|
|
53
53
|
|
|
54
54
|
```bash
|
|
55
55
|
codex="${CODEX_THREAD_ID:-}"; claude="${CLAUDE_CODE_SESSION_ID:-}"
|
|
@@ -78,7 +78,7 @@ for work small enough to skip speccing, delivered inline with no separate plan r
|
|
|
78
78
|
is no intent tier and no stage agent; depth follows the work.
|
|
79
79
|
|
|
80
80
|
`runner step` computes readiness and prints a spawn block per dispatched node - agent, model,
|
|
81
|
-
|
|
81
|
+
node input path, the test command, the call cap - fenced for a session to paste into the Agent
|
|
82
82
|
tool; the runner never spawns (327 D42). `runner status` renders the ledger; `runner answer`
|
|
83
83
|
closes a `needs_decision` node.
|
|
84
84
|
|
|
@@ -165,7 +165,7 @@ ledger is missing (then rebuild it with `Savepoint.rebuild_savepoint`).
|
|
|
165
165
|
| `How plan.md created` / `How checklist.md created` / `Exec started` | Exec (verify plan, matrix, checklist) |
|
|
166
166
|
| `Exec outcome.md created` | Exec done; complete the intent |
|
|
167
167
|
| A terminal savepoint line (`delivered` or `abandoned`) | Terminal; do not resume |
|
|
168
|
-
| A node or `Intent` transition line (`n1 running ...`, `Intent needs_decision ...`) | Exec; a graph delivery is in progress - drive it through `scripts/runner`'s three public verbs, `step` (one turn of the dispatch loop), `status` (renders ledger state, safe to poll constantly), and `answer` (closes a `needs_decision` node) - read node status through `NodeLedger.status` before dispatching anything, never re-derive it by eye |
|
|
168
|
+
| A node or `Intent` transition line (`n1 running ...`, `Intent needs_decision ...`) | Exec; a graph delivery is in progress - drive it through `scripts/runner`'s three public verbs, `step` (one turn of the dispatch loop), `status` (renders ledger state, safe to poll constantly), and `answer` (closes a `needs_decision` node) - read node status through `NodeLedger.status` before dispatching anything, never re-derive it by eye. `scripts/graph-measure` is the read-only sibling over that same ledger, with its own three public verbs `intent`, `budget`, and `cohorts` - it never dispatches and never writes |
|
|
169
169
|
|
|
170
170
|
Filesystem fallback, in order: `checklist.md` with items checked means resume Exec from the
|
|
171
171
|
first unchecked item; `plan.md` plus `checklist.md` means enter Exec; `spec.md` alone means
|
|
@@ -259,8 +259,8 @@ Read `../plastic-conventions/references/completion-and-done.md` for what "intent
|
|
|
259
259
|
4. Review `## Insights` for observations that should become future intents; create them through
|
|
260
260
|
`plastic-intent-creating` and update `chain`.
|
|
261
261
|
5. Close through `plastic-intent-ending`, which runs `scripts/end-intent`: outcome, INDEX,
|
|
262
|
-
savepoint, the store commit, and the disarm (the worktree released, `delivery.lock`
|
|
263
|
-
|
|
262
|
+
savepoint, the store commit, and the disarm (the worktree released, `delivery.lock`
|
|
263
|
+
cleared), then the QMD reindex last and the single owner
|
|
264
264
|
report. Pass `--session` and `--index-note`:
|
|
265
265
|
```bash
|
|
266
266
|
ruby ~/.plastic/scripts/end-intent --store <store_path> --id <ID> --disposition delivered \
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Deep mechanics behind two spots in `SKILL.md`: how `plastic-lock arm` resolves a session id
|
|
4
4
|
when it takes an intent, and why the End-tail steps in Completion (release the worktree, clear
|
|
5
|
-
the lock,
|
|
6
|
-
|
|
5
|
+
the lock, reindex) run in that exact order. `scripts/lib/arm.rb` carries the arm, disarm, and
|
|
6
|
+
repair operations.
|
|
7
7
|
|
|
8
8
|
## Table of Contents
|
|
9
9
|
|
|
@@ -22,18 +22,16 @@ session-less close resolve to the same key.
|
|
|
22
22
|
Arming acquires the durable `delivery.lock` in the intent dir, keyed by that resolved session
|
|
23
23
|
and stamped with `run_mode`. Ownership is session-keyed, not process-keyed, so the arm command
|
|
24
24
|
exiting is fine by construction: the lock stays yours for every later tool call in this
|
|
25
|
-
session, and the record hook refreshes its lease on every write you make.
|
|
26
|
-
the intent id into the session pointer (`~/.plastic/store/.tmp/<session>/current`), which is
|
|
25
|
+
session, and the record hook refreshes its lease on every write you make. Holding that lock is
|
|
27
26
|
why the capture and record hooks stop treating your prompts as day-ledger items for the rest
|
|
28
27
|
of the delivery.
|
|
29
28
|
|
|
30
29
|
## Disarm ordering and worktree cleanup rationale
|
|
31
30
|
|
|
32
31
|
`Arm.disarm` runs the ordered End tail: it releases the worktree first, then clears the
|
|
33
|
-
intent's `delivery.lock` as its recorded owner
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
hook keeps heartbeating the lock until the lock is gone.
|
|
32
|
+
intent's `delivery.lock` as its recorded owner. The worktree goes first so a released lock
|
|
33
|
+
never points at a checkout another session could claim; the lock goes last so the record
|
|
34
|
+
hook keeps heartbeating it until the delivery is actually done.
|
|
37
35
|
|
|
38
36
|
**Mechanized since intent 188.** `scripts/end-intent` performs this disarm itself, as its own
|
|
39
37
|
step 5, after steps 1-4 (outcome/INDEX/savepoint/commit) commit. A pre-flight lock guard
|
|
@@ -52,10 +50,10 @@ survives and can be reclaimed).
|
|
|
52
50
|
When the work is being shipped through a release, do NOT rely on this plain remove.
|
|
53
51
|
`skills/releasing/SKILL.md` runs its worktree-merge step BEFORE its `end-intent` call, merging
|
|
54
52
|
the intent's code branch (`plastic/{id}--{slug}`) back to the repo's default branch BEFORE
|
|
55
|
-
the worktree is removed, via `Worktree.finish(Arm.
|
|
53
|
+
the worktree is removed, via `Worktree.finish(Arm.delivery(intent_dir: ...), merge: true)`
|
|
56
54
|
(merge-then-remove), so the integrated work is not lost. By the time `end-intent`'s own step 5
|
|
57
|
-
runs afterward, the worktree is already gone (a harmless no-op) and only the delivery lock
|
|
58
|
-
|
|
55
|
+
runs afterward, the worktree is already gone (a harmless no-op) and only the delivery lock is
|
|
56
|
+
left to clear. Never leave an orphaned worktree, and run `git worktree prune`
|
|
59
57
|
if you hit a stale reference.
|
|
60
58
|
|
|
61
59
|
## QMD reindex ordering rationale
|
|
@@ -17,10 +17,10 @@ self-declares its disposition through a `disposition: delivered|abandoned` front
|
|
|
17
17
|
header. The delivered path authors it with the result; the abandoned path authors it with
|
|
18
18
|
the abandonment reason and no longer leaves the scaffolded placeholder sentinel in place.
|
|
19
19
|
|
|
20
|
-
The canonical End tail runs in this order
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
The canonical End tail runs in this order: `outcome.md -> INDEX terminal -> the terminal
|
|
21
|
+
savepoint line -> commit -> disarm (Worktree.release -> Lock.release) -> QMD reindex`, the
|
|
22
|
+
reindex always LAST. Running the reindex last keeps the index from ever referencing a lock
|
|
23
|
+
that disarm just removed.
|
|
24
24
|
|
|
25
25
|
`scripts/end-intent` performs this order's disarm step (verify the code worktree is clean,
|
|
26
26
|
then merge/remove worktrees, then clear the lock) as its own step 5, mechanically, since
|
|
@@ -31,12 +31,11 @@ foreign session, reclaims a stale one with an audit line), and a dirty code work
|
|
|
31
31
|
refuses before removal rather than force-discarding uncommitted changes.
|
|
32
32
|
|
|
33
33
|
The post-done access window is lock-bounded: `[INDEX terminal -> Lock.release]`. Through it
|
|
34
|
-
the completing session keeps full read and write access to the terminal directory
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
still present or stale). Finishing the tail is FINISHING a completion, never a reactivation:
|
|
34
|
+
the completing session keeps full read and write access to the terminal directory (108's
|
|
35
|
+
lock-held keep-guard holds it open while `delivery.lock` exists). Once the lock is released
|
|
36
|
+
the window closes and the directory is frozen. A crash mid-tail is recovered by stale-lock
|
|
37
|
+
reclaim plus finishing the tail; `doctor` surfaces this as a "stalled completion" (terminal in
|
|
38
|
+
INDEX but the lock is still present or stale). Finishing the tail is FINISHING a completion, never a reactivation:
|
|
40
39
|
a done intent is never moved back to `## Active`.
|
|
41
40
|
|
|
42
41
|
One report per audience: a delivery produces `outcome.md` plus one EM-to-CTO owner report, and
|
|
@@ -45,3 +45,12 @@ This chapter holds the linking doctrine from Frontmatter and the branch-vs-root
|
|
|
45
45
|
the relation on the PREDECESSOR's `chain` (and mirror it as a
|
|
46
46
|
`[[id--slug|<target's full intent: text>]]` wikilink in `## Links`).
|
|
47
47
|
- **Rule of thumb:** if the intent could exist without its parent, it's a root.
|
|
48
|
+
|
|
49
|
+
## Naming
|
|
50
|
+
|
|
51
|
+
A thing is named after the concept family it lives under. A node is a graph-engineering
|
|
52
|
+
concept, so its name comes from graph engineering (node, edge, ready set, critical path),
|
|
53
|
+
from the Plastic concepts coined on top of it (intent, ledger, node input, lease, gate, runner),
|
|
54
|
+
and from the software and AI engineering concepts those rest on (review, fix, test, verify,
|
|
55
|
+
dispatch, executor, reviewer). A name from outside that stack is refused. Where no existing
|
|
56
|
+
concept fits, that is a design finding to raise, not a word to coin.
|