@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
|
@@ -4,10 +4,10 @@ This chapter holds the delivery lock, claims, worktrees, the fail-safe doctrine,
|
|
|
4
4
|
|
|
5
5
|
### Delivery Isolation and the Single-Owner Lock
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
The delivery lock names the session delivering an intent, as its owner or a delegate. Locks
|
|
8
|
+
and worktrees exist only for auto teams: an interactive session working direct or thinking
|
|
9
|
+
takes no lock and records into its day ledger instead (the oldest day in the last seven whose
|
|
10
|
+
checklist carries the session's line, else today).
|
|
11
11
|
|
|
12
12
|
For an auto team, exactly one team develops an intent's delivery at a time. Ownership is
|
|
13
13
|
session-keyed and durable: arming acquires `delivery.lock` inside the intent directory
|
|
@@ -17,9 +17,7 @@ but never grants access and is never inferred from transcripts or filesystem pat
|
|
|
17
17
|
fields on legacy locks display as `Unknown`. Liveness is a lease: the record hook refreshes
|
|
18
18
|
the lock file's mtime on every write the owning session makes, and that mtime is the sole
|
|
19
19
|
heartbeat truth. The lock counts as stale only when the mtime is older than the TTL. No
|
|
20
|
-
process id is consulted anywhere. The
|
|
21
|
-
records into; the lock file is the truth of who owns a delivery, and wins on any
|
|
22
|
-
disagreement. Another team that finds a fresh lock backs off; a stale lock is reclaimed only
|
|
20
|
+
process id is consulted anywhere. The lock file is the truth of who owns a delivery. Another team that finds a fresh lock backs off; a stale lock is reclaimed only
|
|
23
21
|
by explicit takeover, which replaces the lock and appends an audit line to the intent's
|
|
24
22
|
savepoint.md. Rearming the same session preserves its acquired identity and refreshes known
|
|
25
23
|
provenance; an explicit takeover replaces the controller and starts new provenance.
|
|
@@ -31,7 +29,7 @@ retained as descriptive history, bounded to the 20 most recent terminal entries.
|
|
|
31
29
|
a delegate, and an artifact claim are distinct evidence: controller ownership authorizes the
|
|
32
30
|
delivery, delegate registration authorizes a child session, and a claim selects one current
|
|
33
31
|
writer for one artifact. Disarm clears the lock; the End tail is ordered: verify, merge and
|
|
34
|
-
remove worktrees, clear the lock
|
|
32
|
+
remove worktrees, then clear the lock. Repair
|
|
35
33
|
is one idempotent function with two entry points: the `plastic-lock` command (`who`, status,
|
|
36
34
|
fix, release, reclaim, delegate) and the `plastic-doctor` skill's lock section, so repair
|
|
37
35
|
self-heals. `who` is read-only and reports the controller, mtime heartbeat, delegates, and
|
|
@@ -90,18 +88,18 @@ an orphaned worktree behind, and clear a stale worktree reference with `git work
|
|
|
90
88
|
|
|
91
89
|
#### Intent delivery, station by station
|
|
92
90
|
|
|
93
|
-
How one auto-team intent travels from boarding to the End tail, and what the lock
|
|
91
|
+
How one auto-team intent travels from boarding to the End tail, and what the lock and
|
|
94
92
|
the record hook do at each station. Nothing in the third column blocks; the fourth column is
|
|
95
93
|
what gets written down.
|
|
96
94
|
|
|
97
|
-
| Station | Delivered artifact | Lock
|
|
95
|
+
| Station | Delivered artifact | Lock steps | Record |
|
|
98
96
|
|---|---|---|---|
|
|
99
|
-
| Start (board) | none (a procedure, not a stage) | `plastic-lock fix` self-heals stale, corrupt, or legacy state; arm acquires `delivery.lock` (O_EXCL, session-keyed), provisions the code worktree
|
|
97
|
+
| Start (board) | none (a procedure, not a stage) | `plastic-lock fix` self-heals stale, corrupt, or legacy state; arm acquires `delivery.lock` (O_EXCL, session-keyed), provisions the code worktree | savepoint confirms the boarding station |
|
|
100
98
|
| What (create) | `<id>--<slug>.md`, born complete | no lock yet; `new-intent` validates the file it writes (`scripts/validate-intent`) | savepoint `What` line; intent listed in INDEX `## Active` |
|
|
101
99
|
| Why | `spec.md` | owner writes refresh the lease (lock file mtime heartbeat) | savepoint `Why started`, `Why spec.md created` |
|
|
102
100
|
| How | `plan.md`, `actions/ACTION_N.md` (at least one), `checklist.md` | heartbeat on writes | savepoint `How started`, `How plan.md created`, `How checklist.md created`, `Exec started` |
|
|
103
101
|
| Exec | code on the intent branch, checklist checked off | heartbeat; code edits confined to the provisioned worktree; delegates write under the owner's lock | checklist boxes; savepoint milestones; the day-ledger line promotes when a project file lands |
|
|
104
|
-
| End (done) | mandatory `outcome.md` (`disposition: delivered\|abandoned`), INDEX moves to Completed or Abandoned | ordered End tail: verify, merge and remove worktrees, disarm clears `delivery.lock`,
|
|
102
|
+
| End (done) | mandatory `outcome.md` (`disposition: delivered\|abandoned`), INDEX moves to Completed or Abandoned | ordered End tail: verify, merge and remove worktrees, disarm clears `delivery.lock`, and the QMD reindex runs LAST; `end-intent` backfills a placeholder `outcome.md` from the record and its structure check reports (never refuses) | the savepoint's terminal `delivered` (or `abandoned`) line; takeover audits, if any, remain in savepoint.md |
|
|
105
103
|
| Maintenance (Future, Terminal, or Active-with-a-stale-or-no-lock) | `revisions.md` move-and-record entries | detects (never acquires) `delivery.lock`; defers and reports while the target's lock is FRESH (`Lock.fresh?`); a stale or absent lock is not-active, maintenance proceeds | append-only, rule-tagged `revisions.md` entry written in the same operation as the change, or the change is refused; lands via a fresh branch off store main merged back as one closed op, never `git add -A` |
|
|
106
104
|
|
|
107
105
|
## The write guard is not residue
|
package/skills/direct/SKILL.md
CHANGED
|
@@ -54,8 +54,8 @@ the 15 observable signals and the response each one selects.
|
|
|
54
54
|
item.
|
|
55
55
|
- Only a prompt that changes something on disk or produces an artifact becomes a checklist item.
|
|
56
56
|
A pure question is answered inline and recorded nowhere.
|
|
57
|
-
- Direct work records into the day ledger
|
|
58
|
-
|
|
57
|
+
- Direct work records into the day ledger for the session's day (the oldest day in the last
|
|
58
|
+
seven whose checklist carries the session's line, else today). Never write that lookup by hand.
|
|
59
59
|
|
|
60
60
|
## 5. What direct does not take
|
|
61
61
|
|
package/skills/doctor/SKILL.md
CHANGED
|
@@ -268,7 +268,7 @@ the former locking skill here):
|
|
|
268
268
|
| Verb | What it does | When |
|
|
269
269
|
|---|---|---|
|
|
270
270
|
| `who` | Owner, heartbeat, claims, delegates, from durable files only | Safe inspection; needs `--intent-dir` |
|
|
271
|
-
| `status` | Lock file, freshness, the derived worktree, whether this session
|
|
271
|
+
| `status` | Lock file, freshness, the derived worktree, whether this session is delivering the intent, claims | Always safe; run first |
|
|
272
272
|
| `fix` | Idempotent repair from disk truth for this session; never touches a fresh foreign lock | Interrupted work, corrupt state, `/tmp` wiped |
|
|
273
273
|
| `release` | The owner clears the lock | Ending or abandoning an auto delivery |
|
|
274
274
|
| `reclaim` | Explicit takeover of a stale lock; appends an audit line to `savepoint.md` | The owner is gone and the lease expired |
|
|
@@ -26,7 +26,7 @@ touching the worktree above.
|
|
|
26
26
|
|
|
27
27
|
`ruby scripts/runner step <intent_dir>` computes which nodes in `graph.md`/`nodes/*.md` are
|
|
28
28
|
ready, applies dispatch policy (model, call cap), and prints a spawn block per dispatched node
|
|
29
|
-
- agent, model,
|
|
29
|
+
- agent, model, node input path, the one test command, the call cap - fenced in its own stdout.
|
|
30
30
|
The runner itself never spawns an agent (327 D42). Call `step` again after each dispatched
|
|
31
31
|
node returns.
|
|
32
32
|
|
|
@@ -293,11 +293,11 @@ branch, then removes the worktree, and prunes the repo:
|
|
|
293
293
|
|
|
294
294
|
```bash
|
|
295
295
|
ruby -r ~/.plastic/scripts/lib/worktree -r ~/.plastic/scripts/lib/arm -e \
|
|
296
|
-
'Worktree.finish(Arm.
|
|
296
|
+
'Worktree.finish(Arm.delivery(intent_dir: "<STORE>/<dir>"), merge: true)'
|
|
297
297
|
```
|
|
298
298
|
|
|
299
299
|
(The worktree block is derived from `projects.yml` and the intent id, so the one-liner needs
|
|
300
|
-
only the intent directory
|
|
300
|
+
only the intent directory.)
|
|
301
301
|
|
|
302
302
|
Honor the worktree-cleanup rule: never leave an orphaned worktree, and run `git worktree
|
|
303
303
|
prune` in the affected repo if you hit a stale reference. For why this is the one place the
|
|
@@ -13,7 +13,7 @@ for why the intent's worktree is merged before removal.
|
|
|
13
13
|
## Worktree merge-then-remove rationale
|
|
14
14
|
|
|
15
15
|
**Worktree-isolated intents (intent 73c3).** When the intent was delivered in a Plastic
|
|
16
|
-
worktree (the
|
|
16
|
+
worktree (the delivery lock has a provisioned `worktree` block), its code lives on the branch
|
|
17
17
|
`plastic/{id}--{slug}` inside `<repo>/.claude/worktrees/{id}--{slug}`, not on a hand-made
|
|
18
18
|
feature branch. The merge-then-remove of that worktree is handled together with cleanup in
|
|
19
19
|
Workflow step 9, which merges `plastic/{id}--{slug}` into the default branch BEFORE removing the
|
|
@@ -29,7 +29,7 @@ Work rides the beta-verified lane when it changes operational substrate, carries
|
|
|
29
29
|
migration, lock, or state-format risk, or cannot be fully validated by a hermetic suite alone.
|
|
30
30
|
Everything else merges straight to main.
|
|
31
31
|
|
|
32
|
-
Intent 41's DB layer is the archetypal beta-lane case: it replaces the
|
|
32
|
+
Intent 41's DB layer is the archetypal beta-lane case: it replaces the lock and session
|
|
33
33
|
file formats with a new persistent SQLite substrate. A green suite proves the code correct; it
|
|
34
34
|
cannot prove the new substrate survives real, uncontrolled usage, so real-use verification on
|
|
35
35
|
beta comes first.
|
package/templates/index.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## Active
|
|
4
4
|
<!-- Line shape: `- [ID — Title](path)`. Writes emit the em-dash (U+2014) between
|
|
5
|
-
id and title; readers (
|
|
5
|
+
id and title; readers (IndexEntry.match) accept the em-dash or a plain
|
|
6
6
|
hyphen, so either form parses (see https://github.com/zalom/plastic/blob/main/docs/internals.md). -->
|
|
7
7
|
(no active intents)
|
|
8
8
|
|
package/templates/project.yml
CHANGED
|
@@ -8,5 +8,5 @@ release:
|
|
|
8
8
|
# mode: direct # direct | pull_request
|
|
9
9
|
# base: main # defaults to origin/HEAD, then main, then master
|
|
10
10
|
# branch_template: "session/{{day}}" # {{day}}, {{ticket}}, {{slug}} tokens
|
|
11
|
-
# ticket_source: intent_id # intent_id (the
|
|
11
|
+
# ticket_source: intent_id # intent_id (the delivering intent's id from its delivery lock, else the day id)
|
|
12
12
|
# workspace: checkout # checkout | worktree (worktree applies to direct mode only)
|
package/scripts/lib/bridge.rb
DELETED
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env ruby
|
|
2
|
-
# encoding: UTF-8
|
|
3
|
-
|
|
4
|
-
require "yaml"
|
|
5
|
-
require_relative "lock"
|
|
6
|
-
|
|
7
|
-
# Bridge - the shared helpers that outlived the /tmp bridge JSON.
|
|
8
|
-
#
|
|
9
|
-
# Until 2.0 this file cached a session's delivery state in
|
|
10
|
-
# `<tmp>/plastic-<session>--<id>.json` (stage, worktree, lock, auto flag) and
|
|
11
|
-
# arbitrated which bridge a gate hook should read. Ruling 6 of intent 296
|
|
12
|
-
# retired that: the per-session pointer (`~/.plastic/store/.tmp/<session>/current`)
|
|
13
|
-
# plus `delivery.lock` in the intent directory is the whole bridge, and
|
|
14
|
-
# `scripts/lib/arm.rb` is how a team takes and gives back an intent (intent
|
|
15
|
-
# 307). What stays here is the handful of pure helpers a dozen callers still
|
|
16
|
-
# share: the INDEX entry matcher, the Active check, the id-from-dir parser,
|
|
17
|
-
# the project config reader, `blank?`, and the skill-reference delegator.
|
|
18
|
-
module Bridge
|
|
19
|
-
# Single place a skill-reference string gets built (intent 201, D3). The
|
|
20
|
-
# prefix table lives on Lock; this is a thin delegator so call sites read
|
|
21
|
-
# Bridge.skill_ref.
|
|
22
|
-
def self.skill_ref(name, harness: :claude)
|
|
23
|
-
Lock.skill_ref(name, harness: harness)
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def self.blank?(value)
|
|
27
|
-
value.nil? || value.to_s.strip.empty?
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
# --- Shared INDEX entry matcher (intent 188, D12/D13) -----------------------
|
|
31
|
-
#
|
|
32
|
-
# ONE definition site for the "- [ID <sep> Title](link)" shape both
|
|
33
|
-
# `intent_active?` (below) and `scripts/end-intent`'s own INDEX-move parser
|
|
34
|
-
# depend on, so the two regexes can never drift apart. Accepts a real em dash
|
|
35
|
-
# (U+2014) OR a plain hyphen as the id/title separator on READ; every WRITE
|
|
36
|
-
# still emits the real em dash (D10). The separator is built from the
|
|
37
|
-
# codepoint, not a literal byte, so this file stays em-dash free.
|
|
38
|
-
EM_DASH = "\u2014".freeze
|
|
39
|
-
INDEX_ENTRY_RE = /\A- \[(\S+)\s+(?:#{Regexp.escape(EM_DASH)}|-)\s+(.*?)\]\(([^)]+)\)/.freeze
|
|
40
|
-
|
|
41
|
-
# Match `line` (already chomped) against the shared INDEX entry shape.
|
|
42
|
-
# Returns a MatchData (captures: 1 = id, 2 = title, 3 = link) or nil.
|
|
43
|
-
def self.index_entry_match(line)
|
|
44
|
-
line.to_s.match(INDEX_ENTRY_RE)
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
# True iff the intent is Active in its store's INDEX.md, which lives at the
|
|
48
|
-
# PARENT of the store/ dir. Non-raising: any failure (missing or unreadable
|
|
49
|
-
# INDEX, bad arg) returns false. `index_active_ids` is a pure-data test
|
|
50
|
-
# seam: when an Array of id strings is supplied, membership is checked
|
|
51
|
-
# against it directly with no file read.
|
|
52
|
-
def self.intent_active?(intent_id, store:, index_active_ids: nil)
|
|
53
|
-
target = intent_id.to_s
|
|
54
|
-
return index_active_ids.include?(target) if index_active_ids.is_a?(Array)
|
|
55
|
-
|
|
56
|
-
index = File.join(File.dirname(store.to_s), "INDEX.md")
|
|
57
|
-
return false unless File.exist?(index)
|
|
58
|
-
|
|
59
|
-
in_active = false
|
|
60
|
-
File.foreach(index) do |line|
|
|
61
|
-
stripped = line.chomp
|
|
62
|
-
if stripped == "## Active"
|
|
63
|
-
in_active = true
|
|
64
|
-
next
|
|
65
|
-
end
|
|
66
|
-
next unless in_active
|
|
67
|
-
break if stripped.start_with?("## ") # next section ends the Active block
|
|
68
|
-
m = index_entry_match(stripped)
|
|
69
|
-
return true if m && m[1] == target
|
|
70
|
-
end
|
|
71
|
-
false
|
|
72
|
-
rescue StandardError
|
|
73
|
-
false
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
# --- Project config -------------------------------------------------------------
|
|
77
|
-
|
|
78
|
-
PROJECT_CONFIG_DEFAULTS = {
|
|
79
|
-
"governing_docs" => ["AGENTS.md"],
|
|
80
|
-
"release" => {
|
|
81
|
-
"on_complete" => "commit",
|
|
82
|
-
},
|
|
83
|
-
}.freeze
|
|
84
|
-
|
|
85
|
-
def self.read_project_config(slug)
|
|
86
|
-
path = File.join(Dir.home, ".plastic", "projects", slug, "project.yml")
|
|
87
|
-
config = if File.exist?(path)
|
|
88
|
-
YAML.safe_load(File.read(path)) || {}
|
|
89
|
-
else
|
|
90
|
-
{}
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
deep_merge(PROJECT_CONFIG_DEFAULTS, config)
|
|
94
|
-
rescue => e
|
|
95
|
-
$stderr.puts "Warning: failed to read project config for #{slug}: #{e.message}"
|
|
96
|
-
PROJECT_CONFIG_DEFAULTS.dup
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
# "<id>" from a ".../store/<id>--<slug>" dir, else nil.
|
|
100
|
-
def self.intent_id_from_dir(dir)
|
|
101
|
-
base = File.basename(dir.to_s)
|
|
102
|
-
base.include?("--") ? base.split("--", 2).first : nil
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
def self.deep_merge(base, overlay)
|
|
106
|
-
result = base.dup
|
|
107
|
-
overlay.each do |key, value|
|
|
108
|
-
if value.is_a?(Hash) && result[key].is_a?(Hash)
|
|
109
|
-
result[key] = deep_merge(result[key], value)
|
|
110
|
-
else
|
|
111
|
-
result[key] = value
|
|
112
|
-
end
|
|
113
|
-
end
|
|
114
|
-
result
|
|
115
|
-
end
|
|
116
|
-
end
|