@zalom/plastic 2.0.0-alpha.23 → 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-node-research.md +4 -4
- package/agents/plastic-node-verify.md +4 -4
- package/agents/plastic-node-work.md +4 -4
- package/package.json +1 -1
- package/scripts/dashboard.rb +1 -1
- package/scripts/end-intent +19 -16
- package/scripts/exec-worktree +5 -5
- package/scripts/hook-call-budget +6 -6
- package/scripts/hook-capture +15 -15
- package/scripts/hook-record +21 -14
- package/scripts/hook-savepoint +2 -1
- package/scripts/hook-session-start +9 -10
- package/scripts/lib/active_delivery.rb +44 -0
- package/scripts/lib/arm.rb +41 -102
- package/scripts/lib/codex_adapter.rb +2 -2
- package/scripts/lib/{packet_wrapper.rb → data_boundary.rb} +7 -7
- package/scripts/lib/day_summary.rb +19 -11
- package/scripts/lib/doctor_session_ledger.rb +3 -52
- package/scripts/lib/exec_worktree.rb +24 -21
- package/scripts/lib/graph_measure_budget.rb +29 -28
- package/scripts/lib/handoff.rb +4 -8
- package/scripts/lib/harness_adapter.rb +6 -6
- package/scripts/lib/index_entry.rb +53 -0
- package/scripts/lib/insights.rb +1 -1
- package/scripts/lib/installer_core.rb +120 -7
- package/scripts/lib/lock.rb +8 -9
- 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 +4 -2
- package/scripts/lib/outcome_report.rb +1 -1
- package/scripts/lib/project_config.rb +45 -0
- package/scripts/lib/ready_set.rb +1 -1
- package/scripts/lib/roadmap_savepoint.rb +1 -1
- package/scripts/lib/runner_absorb.rb +5 -5
- package/scripts/lib/runner_dispatch.rb +44 -44
- package/scripts/lib/runner_sweep.rb +4 -4
- package/scripts/lib/runner_until_empty.rb +1 -1
- package/scripts/lib/savepoint.rb +6 -7
- package/scripts/lib/scaffold_intent.rb +6 -3
- package/scripts/lib/session_close.rb +30 -28
- package/scripts/lib/session_git.rb +20 -14
- package/scripts/lib/session_ledger.rb +44 -4
- package/scripts/lib/worktree.rb +24 -24
- package/scripts/lib/worktree_sweep.rb +3 -3
- package/scripts/{node-packet → node-input} +15 -15
- package/scripts/node-run +19 -19
- package/scripts/plastic-lock +33 -32
- package/scripts/runner +1 -1
- package/skills/auto/SKILL.md +7 -7
- 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 +1 -1
- package/skills/conventions/references/locks-and-worktrees.md +10 -12
- package/skills/direct/SKILL.md +2 -2
- package/skills/doctor/SKILL.md +1 -1
- package/skills/intent-executing/SKILL.md +1 -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/templates/index.md +1 -1
- package/templates/project.yml +1 -1
- package/scripts/lib/bridge.rb +0 -116
package/scripts/lib/worktree.rb
CHANGED
|
@@ -108,16 +108,16 @@ module Worktree
|
|
|
108
108
|
|
|
109
109
|
# --- provisioning ----------------------------------------------------------
|
|
110
110
|
|
|
111
|
-
# Resolve the slug from the
|
|
111
|
+
# Resolve the slug from the delivery's intent.store, create code + store
|
|
112
112
|
# worktrees (idempotent: reuse an existing worktree path, do not error), write
|
|
113
|
-
# the `worktree` block plus `provisioned: true` onto
|
|
113
|
+
# the `worktree` block plus `provisioned: true` onto delivery, return it.
|
|
114
114
|
#
|
|
115
115
|
# Fails open with a stderr log when the repo is non-git or unresolvable:
|
|
116
116
|
# sets `provisioned: false` and leaves `code: null`. All git ops use
|
|
117
117
|
# `git -C <resolved path>` -- never cwd (decision D6).
|
|
118
|
-
def provision(
|
|
119
|
-
return
|
|
120
|
-
intent =
|
|
118
|
+
def provision(delivery, home: Dir.home, runner: ShellRunner.new)
|
|
119
|
+
return delivery unless delivery.is_a?(Hash)
|
|
120
|
+
intent = delivery["intent"] || {}
|
|
121
121
|
intent_id = intent["id"].to_s
|
|
122
122
|
store = intent["store"].to_s
|
|
123
123
|
intent_slug = slug_from_dir(intent["dir"]) || slug_from_dir(store)
|
|
@@ -173,15 +173,15 @@ module Worktree
|
|
|
173
173
|
# replaced by the fresh unprovisioned block; the guard must not fail harder
|
|
174
174
|
# than the bug it guards.
|
|
175
175
|
if code_ok
|
|
176
|
-
|
|
176
|
+
delivery["worktree"] = block
|
|
177
177
|
else
|
|
178
|
-
existing =
|
|
178
|
+
existing = delivery["worktree"]
|
|
179
179
|
keep = existing.is_a?(Hash) && !blank?(existing["code"]) &&
|
|
180
180
|
Dir.exist?(existing["code"].to_s)
|
|
181
|
-
|
|
181
|
+
delivery["worktree"] = block unless keep
|
|
182
182
|
end
|
|
183
183
|
|
|
184
|
-
|
|
184
|
+
delivery
|
|
185
185
|
end
|
|
186
186
|
|
|
187
187
|
# Remove the worktree (then `git worktree prune`), clear the worktree block.
|
|
@@ -189,21 +189,21 @@ module Worktree
|
|
|
189
189
|
# policy on top via `finish`; this is the plain remove. Pass `remove: false` to
|
|
190
190
|
# clear the block WITHOUT touching git (so `finish` can merge first, then call
|
|
191
191
|
# release to drop the worktree once the code branch is integrated).
|
|
192
|
-
def release(
|
|
193
|
-
return
|
|
194
|
-
block =
|
|
195
|
-
return
|
|
192
|
+
def release(delivery, home: Dir.home, runner: ShellRunner.new, remove: true)
|
|
193
|
+
return delivery unless delivery.is_a?(Hash)
|
|
194
|
+
block = delivery["worktree"]
|
|
195
|
+
return delivery unless block.is_a?(Hash)
|
|
196
196
|
|
|
197
197
|
if remove
|
|
198
|
-
slug = slug_for_store(
|
|
198
|
+
slug = slug_for_store(delivery.dig("intent", "store").to_s, home: home)
|
|
199
199
|
repo = repo_for(slug, home: home)
|
|
200
200
|
|
|
201
201
|
remove_worktree(runner, repo: repo, worktree: block["code"]) if repo && block["code"]
|
|
202
202
|
prune(runner, repo: repo) if repo
|
|
203
203
|
end
|
|
204
204
|
|
|
205
|
-
|
|
206
|
-
|
|
205
|
+
delivery.delete("worktree")
|
|
206
|
+
delivery
|
|
207
207
|
end
|
|
208
208
|
|
|
209
209
|
# --- cleanup policy (merge-vs-remove) -------------------------------------
|
|
@@ -222,19 +222,19 @@ module Worktree
|
|
|
222
222
|
# Fail-open and idempotent throughout: a missing block, missing branch, or any
|
|
223
223
|
# git failure never raises and never blocks teardown. All git ops use
|
|
224
224
|
# `git -C <path>`, never cwd (decision D6). No-op when nothing was provisioned.
|
|
225
|
-
def finish(
|
|
226
|
-
return
|
|
227
|
-
block =
|
|
228
|
-
return
|
|
225
|
+
def finish(delivery, home: Dir.home, runner: ShellRunner.new, merge: false)
|
|
226
|
+
return delivery unless delivery.is_a?(Hash)
|
|
227
|
+
block = delivery["worktree"]
|
|
228
|
+
return delivery unless block.is_a?(Hash)
|
|
229
229
|
|
|
230
230
|
if merge
|
|
231
|
-
slug = slug_for_store(
|
|
231
|
+
slug = slug_for_store(delivery.dig("intent", "store").to_s, home: home)
|
|
232
232
|
repo = repo_for(slug, home: home)
|
|
233
233
|
branch = block["code_branch"]
|
|
234
234
|
merge_branch(runner, repo: repo, branch: branch) if repo && !blank?(branch)
|
|
235
235
|
end
|
|
236
236
|
|
|
237
|
-
release(
|
|
237
|
+
release(delivery, home: home, runner: runner, remove: true)
|
|
238
238
|
end
|
|
239
239
|
|
|
240
240
|
# Merge `branch` into the repo's default branch from the main checkout. The
|
|
@@ -298,8 +298,8 @@ module Worktree
|
|
|
298
298
|
# --- lock ------------------------------------------------------------------
|
|
299
299
|
|
|
300
300
|
# True iff ANOTHER session's delivery.lock is FRESH on this intent's dir
|
|
301
|
-
# (intent 108, D2): the durable lock file decides
|
|
302
|
-
#
|
|
301
|
+
# (intent 108, D2): the durable lock file decides alone, and no pid is
|
|
302
|
+
# probed. current_session being the owner or a
|
|
303
303
|
# delegate does not count as "other". A stale lock does not hold (explicit
|
|
304
304
|
# takeover reclaims it).
|
|
305
305
|
def lock_held_by_other?(intent_id:, store:, current_session:, home: Dir.home,
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
4
|
require_relative "worktree"
|
|
5
|
-
require_relative "
|
|
5
|
+
require_relative "index_entry"
|
|
6
6
|
|
|
7
7
|
# WorktreeSweep -- the one-time orphan sweep for store worktrees retired by
|
|
8
8
|
# intent 178 (D2). Pure candidate classification plus a thin apply step, both
|
|
@@ -62,7 +62,7 @@ module WorktreeSweep
|
|
|
62
62
|
|
|
63
63
|
# The INDEX.md section heading (e.g. "Active", "Completed") that lists this
|
|
64
64
|
# intent dir, or nil if no INDEX.md entry links to it. Reuses
|
|
65
|
-
#
|
|
65
|
+
# IndexEntry.match so this never drifts from the shared parser.
|
|
66
66
|
def index_status(intent_dir, name)
|
|
67
67
|
store = File.dirname(intent_dir)
|
|
68
68
|
index = File.join(File.dirname(store), "INDEX.md")
|
|
@@ -75,7 +75,7 @@ module WorktreeSweep
|
|
|
75
75
|
section = stripped.sub(/\A##\s*/, "").strip
|
|
76
76
|
next
|
|
77
77
|
end
|
|
78
|
-
m =
|
|
78
|
+
m = IndexEntry.match(stripped)
|
|
79
79
|
next unless m
|
|
80
80
|
return section if m[3].to_s.include?(name)
|
|
81
81
|
end
|
|
@@ -2,30 +2,30 @@
|
|
|
2
2
|
# encoding: UTF-8
|
|
3
3
|
# frozen_string_literal: true
|
|
4
4
|
|
|
5
|
-
require_relative "lib/
|
|
5
|
+
require_relative "lib/node_input"
|
|
6
6
|
require_relative "lib/savepoint"
|
|
7
7
|
|
|
8
|
-
# node-
|
|
9
|
-
# whole input from disk and writes it to
|
|
8
|
+
# node-input - the CLI over NodeInput (intent 338, G5). Builds one node's
|
|
9
|
+
# whole input from disk and writes it to attempts/<node>--a<N>.input (or
|
|
10
10
|
# --out), printing a parsable summary and the exact node-transition running
|
|
11
11
|
# command intent 340's runner should record.
|
|
12
12
|
#
|
|
13
13
|
# Usage:
|
|
14
|
-
# node-
|
|
14
|
+
# node-input <intent_dir> --node <id> [--budget N] [--hop-tokens N]
|
|
15
15
|
# [--holder H] [--expires E] [--model M] [--attempt N]
|
|
16
16
|
# [--out PATH] [--force]
|
|
17
17
|
#
|
|
18
18
|
# Exit codes (spec D17, shared with node-transition's family):
|
|
19
|
-
# 0 - the
|
|
19
|
+
# 0 - the node input was written (or the identical bytes already existed)
|
|
20
20
|
# 2 - usage: not an intent directory, missing --node, or an unknown node
|
|
21
21
|
# 3 - an unreadable, cyclic or unparsable graph, node file, or record
|
|
22
22
|
# 4 - overflow past the third cut; nothing was written
|
|
23
23
|
# 5 - an existing attempt file whose bytes differ; nothing was written
|
|
24
|
-
module
|
|
24
|
+
module NodeInputCLI
|
|
25
25
|
module_function
|
|
26
26
|
|
|
27
27
|
def usage
|
|
28
|
-
warn "Usage: node-
|
|
28
|
+
warn "Usage: node-input <intent_dir> --node <id> [--budget N] [--hop-tokens N] " \
|
|
29
29
|
"[--holder H] [--expires E] [--model M] [--attempt N] [--out PATH] [--force]"
|
|
30
30
|
end
|
|
31
31
|
|
|
@@ -42,7 +42,7 @@ module NodePacketCLI
|
|
|
42
42
|
intent_dir_arg = args.shift
|
|
43
43
|
|
|
44
44
|
unless intent_directory?(intent_dir_arg && File.expand_path(intent_dir_arg))
|
|
45
|
-
warn "node-
|
|
45
|
+
warn "node-input: #{intent_dir_arg.inspect} is not an intent directory"
|
|
46
46
|
usage
|
|
47
47
|
exit 2
|
|
48
48
|
end
|
|
@@ -50,7 +50,7 @@ module NodePacketCLI
|
|
|
50
50
|
|
|
51
51
|
node = opt(args, "--node")
|
|
52
52
|
if node.to_s.strip.empty?
|
|
53
|
-
warn "node-
|
|
53
|
+
warn "node-input: --node is required"
|
|
54
54
|
usage
|
|
55
55
|
exit 2
|
|
56
56
|
end
|
|
@@ -64,11 +64,11 @@ module NodePacketCLI
|
|
|
64
64
|
model = opt(args, "--model")
|
|
65
65
|
force = args.include?("--force")
|
|
66
66
|
|
|
67
|
-
result =
|
|
67
|
+
result = NodeInput.build(
|
|
68
68
|
intent_dir: intent_dir,
|
|
69
69
|
node: node,
|
|
70
|
-
budget_tokens: budget ? budget.to_i :
|
|
71
|
-
hop_tokens: hop_tokens ? hop_tokens.to_i :
|
|
70
|
+
budget_tokens: budget ? budget.to_i : NodeInput::DEFAULT_BUDGET_TOKENS,
|
|
71
|
+
hop_tokens: hop_tokens ? hop_tokens.to_i : NodeInput::DEFAULT_HOP_TOKENS,
|
|
72
72
|
holder: holder,
|
|
73
73
|
expires: expires,
|
|
74
74
|
model: model,
|
|
@@ -78,15 +78,15 @@ module NodePacketCLI
|
|
|
78
78
|
)
|
|
79
79
|
|
|
80
80
|
if result[:ok]
|
|
81
|
-
puts
|
|
81
|
+
puts NodeInput.summary_line(result)
|
|
82
82
|
puts result[:running_command]
|
|
83
83
|
exit 0
|
|
84
84
|
end
|
|
85
85
|
|
|
86
|
-
Array(result[:errors]).each { |e| warn "node-
|
|
86
|
+
Array(result[:errors]).each { |e| warn "node-input: #{e}" }
|
|
87
87
|
puts result[:needs_decision_command] if result[:needs_decision_command]
|
|
88
88
|
exit result[:exit_code]
|
|
89
89
|
end
|
|
90
90
|
end
|
|
91
91
|
|
|
92
|
-
|
|
92
|
+
NodeInputCLI.main(ARGV) if $PROGRAM_NAME == __FILE__
|
package/scripts/node-run
CHANGED
|
@@ -8,7 +8,7 @@ require "yaml"
|
|
|
8
8
|
require "fileutils"
|
|
9
9
|
require_relative "lib/savepoint"
|
|
10
10
|
require_relative "lib/node_ledger"
|
|
11
|
-
require_relative "lib/
|
|
11
|
+
require_relative "lib/node_input"
|
|
12
12
|
require_relative "lib/node_worktree"
|
|
13
13
|
require_relative "lib/node_file"
|
|
14
14
|
require_relative "lib/ready_set"
|
|
@@ -19,9 +19,9 @@ require_relative "lib/codex_adapter"
|
|
|
19
19
|
#
|
|
20
20
|
# On Claude Code a subagent dispatch IS the executor; on Codex the executor
|
|
21
21
|
# is a subprocess, so this script owns the timing of a whole node: it reads
|
|
22
|
-
# the node's live `running` line, resolves the
|
|
22
|
+
# the node's live `running` line, resolves the node input that line names,
|
|
23
23
|
# builds the `codex exec` argv for the node's kind (CodexAdapter), runs it
|
|
24
|
-
# in the node's worktree with the
|
|
24
|
+
# in the node's worktree with the node input on stdin, reads the executor's
|
|
25
25
|
# final message, and writes it to the attempt's own return path.
|
|
26
26
|
#
|
|
27
27
|
# It writes NO ledger transition, on any path - success, refusal or error
|
|
@@ -41,8 +41,8 @@ require_relative "lib/codex_adapter"
|
|
|
41
41
|
# 2 - usage: not an intent directory, or missing --node
|
|
42
42
|
# 3 - the node has no live `running` line
|
|
43
43
|
# 4 - the running line is held by another session
|
|
44
|
-
# 5 - the
|
|
45
|
-
# 6 - the
|
|
44
|
+
# 5 - the node input the running line names is missing from disk
|
|
45
|
+
# 6 - the node input on disk does not hash to the running line's input=
|
|
46
46
|
# 7 - no worktree could be resolved to run in
|
|
47
47
|
module NodeRunCLI
|
|
48
48
|
module_function
|
|
@@ -60,12 +60,12 @@ module NodeRunCLI
|
|
|
60
60
|
dir && File.directory?(dir) && File.exist?(Savepoint.intent_file(dir))
|
|
61
61
|
end
|
|
62
62
|
|
|
63
|
-
# <intent_dir>/
|
|
64
|
-
# one per attempt, the same treatment
|
|
63
|
+
# <intent_dir>/attempts/<node>--a<N>.return.yml (spec): beside the node input,
|
|
64
|
+
# one per attempt, the same treatment input files already get - never
|
|
65
65
|
# reused across attempts, so a second attempt's return can never be
|
|
66
66
|
# mistaken for the first's.
|
|
67
67
|
def return_path(intent_dir, node, attempt)
|
|
68
|
-
File.join(intent_dir, "
|
|
68
|
+
File.join(intent_dir, "attempts", "#{node}--a#{attempt}.return.yml")
|
|
69
69
|
end
|
|
70
70
|
|
|
71
71
|
# The node's own declared kind, read straight off its node file - "work"
|
|
@@ -167,25 +167,25 @@ module NodeRunCLI
|
|
|
167
167
|
|
|
168
168
|
kind = resolve_kind(intent_dir, node)
|
|
169
169
|
|
|
170
|
-
# matrix row 6.11: the attempt number comes from
|
|
170
|
+
# matrix row 6.11: the attempt number comes from NodeInput's own
|
|
171
171
|
# counter, never a hand count. `lease_flag_given: false` because the
|
|
172
172
|
# running line this attempt is acting on ALREADY exists on disk (it is
|
|
173
173
|
# what we just read above) - counting it a second time would double it
|
|
174
174
|
# (the same count, plus one, that produced it in the first place when
|
|
175
|
-
# the
|
|
176
|
-
attempt =
|
|
177
|
-
|
|
175
|
+
# the node input was originally built).
|
|
176
|
+
attempt = NodeInput.compute_attempt_number(intent_dir: intent_dir, node: node, lease_flag_given: false)
|
|
177
|
+
input_path = NodeInput.input_path(intent_dir: intent_dir, node: node, attempt: attempt)
|
|
178
178
|
|
|
179
|
-
unless File.exist?(
|
|
180
|
-
warn "node-run: #{node}'s
|
|
179
|
+
unless File.exist?(input_path)
|
|
180
|
+
warn "node-run: #{node}'s node input is missing at #{input_path}"
|
|
181
181
|
exit 5
|
|
182
182
|
end
|
|
183
183
|
|
|
184
|
-
|
|
185
|
-
actual_sha = Digest::SHA256.hexdigest(
|
|
186
|
-
expected_sha = fields["
|
|
184
|
+
input_bytes = File.binread(input_path)
|
|
185
|
+
actual_sha = Digest::SHA256.hexdigest(input_bytes)[0, 12]
|
|
186
|
+
expected_sha = fields["input"]
|
|
187
187
|
unless expected_sha && actual_sha == expected_sha
|
|
188
|
-
warn "node-run: #{node}'s
|
|
188
|
+
warn "node-run: #{node}'s node input at #{input_path} does not hash to input=#{expected_sha.inspect} " \
|
|
189
189
|
"(computed #{actual_sha})"
|
|
190
190
|
exit 6
|
|
191
191
|
end
|
|
@@ -203,7 +203,7 @@ module NodeRunCLI
|
|
|
203
203
|
timeout_seconds = timeout_flag ? timeout_flag.to_i : CodexAdapter.timeout_seconds(kind)
|
|
204
204
|
codex_argv = CodexAdapter.build_argv(kind: kind, worktree: worktree, output_last_message: message_path)
|
|
205
205
|
|
|
206
|
-
result = CodexAdapter.execute(codex_argv, stdin_data:
|
|
206
|
+
result = CodexAdapter.execute(codex_argv, stdin_data: input_bytes, timeout_seconds: timeout_seconds,
|
|
207
207
|
output_last_message_path: message_path)
|
|
208
208
|
|
|
209
209
|
# matrix rows 6.20-6.22: a nonzero exit or a timeout is authoritative -
|
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/runner
CHANGED
|
@@ -412,7 +412,7 @@ module Runner
|
|
|
412
412
|
exit(exit_code)
|
|
413
413
|
end
|
|
414
414
|
|
|
415
|
-
# Row 1.24: lives beside savepoint.md, never inside
|
|
415
|
+
# Row 1.24: lives beside savepoint.md, never inside attempts/ (which the
|
|
416
416
|
# reaper treats as attempt evidence). Row 1.32: registers itself in the
|
|
417
417
|
# store's own top-level `.gitignore` (Worktree.ensure_gitignored's own
|
|
418
418
|
# idempotent, non-raising append) the same way `*.lock` already is, so a
|
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
|
|
|
@@ -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
|
|
@@ -50,7 +50,7 @@ This chapter holds the linking doctrine from Frontmatter and the branch-vs-root
|
|
|
50
50
|
|
|
51
51
|
A thing is named after the concept family it lives under. A node is a graph-engineering
|
|
52
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,
|
|
53
|
+
from the Plastic concepts coined on top of it (intent, ledger, node input, lease, gate, runner),
|
|
54
54
|
and from the software and AI engineering concepts those rest on (review, fix, test, verify,
|
|
55
55
|
dispatch, executor, reviewer). A name from outside that stack is refused. Where no existing
|
|
56
56
|
concept fits, that is a design finding to raise, not a word to coin.
|