@zalom/plastic 1.4.1 → 1.6.0
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-reference.md +2 -0
- package/PLASTIC.md +132 -31
- package/agents/plastic-intent-curator.md +10 -2
- package/package.json +1 -1
- package/scripts/doctor.rb +117 -15
- package/scripts/end-intent +32 -7
- package/scripts/lib/agent_models.rb +25 -0
- package/scripts/lib/bridge.rb +3 -4
- package/scripts/lib/hook_registry.rb +12 -0
- package/scripts/lib/installer_core.rb +18 -6
- package/scripts/lib/maintenance_git.rb +94 -0
- package/scripts/lib/revisions_writer.rb +69 -0
- package/scripts/lib/worktree.rb +14 -32
- package/scripts/lib/worktree_sweep.rb +129 -0
- package/scripts/maintenance-run +236 -0
- package/scripts/project-links +127 -24
- package/scripts/rebuild-graph +37 -3
- package/scripts/restore-intent-v1 +37 -3
- package/scripts/sweep-store-worktrees +53 -0
- package/skills/auto/references/end-tail.md +8 -6
- package/skills/doctor/SKILL.md +6 -0
- package/skills/releasing/SKILL.md +3 -3
- package/skills/store-curating/SKILL.md +9 -0
- package/skills/store-curating/evals/evals.json +16 -0
|
@@ -45,6 +45,7 @@ require_relative "lib/store_discovery"
|
|
|
45
45
|
require_relative "lib/intent_validator"
|
|
46
46
|
require_relative "lib/graph_rebuild"
|
|
47
47
|
require_relative "lib/frontmatter_writer"
|
|
48
|
+
require_relative "lib/links_projection"
|
|
48
49
|
require_relative "lib/restore_intent_v1"
|
|
49
50
|
|
|
50
51
|
class RestoreIntentV1CLI
|
|
@@ -116,8 +117,18 @@ class RestoreIntentV1CLI
|
|
|
116
117
|
|
|
117
118
|
other_files = prose_siblings_to_restore(dir)
|
|
118
119
|
|
|
120
|
+
# `## Links` is a purely derived section: apply_graph always re-derives new_md's body
|
|
121
|
+
# from v1_md's ORIGINAL (pre-reprojection) content, so its Links ENTRY LINES differ
|
|
122
|
+
# from the current, already-reprojected file on every single invocation, even when
|
|
123
|
+
# nothing else changed. Comparing raw new_md != current_md would treat that churn as a
|
|
124
|
+
# real change forever (write, reproject, write, reproject...), which is both pointless
|
|
125
|
+
# IO and, per D14, a false "something changed" signal. md_changed ignores only the
|
|
126
|
+
# entry lines themselves; any other difference (frontmatter graph, real prose) is
|
|
127
|
+
# still a real, detected change.
|
|
128
|
+
md_changed = differs_outside_links_entries?(new_md, current_md)
|
|
129
|
+
|
|
119
130
|
prose_changes = other_files.keys.dup
|
|
120
|
-
prose_changes.unshift("#{base}.md") if
|
|
131
|
+
prose_changes.unshift("#{base}.md") if md_changed
|
|
121
132
|
|
|
122
133
|
puts RestoreIntentV1.render_report(
|
|
123
134
|
base: base, at: at, prose_changes: prose_changes,
|
|
@@ -128,7 +139,7 @@ class RestoreIntentV1CLI
|
|
|
128
139
|
|
|
129
140
|
return unless apply
|
|
130
141
|
|
|
131
|
-
File.write(md_path, new_md) if
|
|
142
|
+
File.write(md_path, new_md) if md_changed
|
|
132
143
|
other_files.each { |f, content| File.write(File.join(dir, f), content) }
|
|
133
144
|
|
|
134
145
|
puts "Reminder: restore-to-v1 runs under the maintenance lock (PLASTIC.md > Terminal " \
|
|
@@ -136,7 +147,8 @@ class RestoreIntentV1CLI
|
|
|
136
147
|
|
|
137
148
|
handle_links_reprojection(base)
|
|
138
149
|
append_revision(dir, base, graph, files: prose_changes,
|
|
139
|
-
before_sources: current_fm["sources"], before_chain: current_fm["chain"])
|
|
150
|
+
before_sources: current_fm["sources"], before_chain: current_fm["chain"]) \
|
|
151
|
+
if md_changed || other_files.any?
|
|
140
152
|
end
|
|
141
153
|
|
|
142
154
|
private
|
|
@@ -261,6 +273,28 @@ class RestoreIntentV1CLI
|
|
|
261
273
|
"Rerun: ruby #{project_links} --plastic-home #{plastic_home}"
|
|
262
274
|
end
|
|
263
275
|
|
|
276
|
+
# True iff `md_a`/`md_b` differ OUTSIDE their ## Links ENTRY LINES (the ones
|
|
277
|
+
# LinksProjection renders/parses, plus its empty-state comment). Line-level, not
|
|
278
|
+
# section-level: a section-boundary replace (LinksSection.rewrite) would discard
|
|
279
|
+
# everything from the heading to EOF, which is unsafe here because a stray prose
|
|
280
|
+
# edit sitting after ## Links would be discarded too, not only the Links entries
|
|
281
|
+
# themselves. Frontmatter and every non-entry body line are compared verbatim, so a
|
|
282
|
+
# genuine graph or prose change is always still detected.
|
|
283
|
+
def differs_outside_links_entries?(md_a, md_b)
|
|
284
|
+
neutralize_links_entries(md_a) != neutralize_links_entries(md_b)
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def neutralize_links_entries(text)
|
|
288
|
+
text.to_s.lines.map do |line|
|
|
289
|
+
stripped = line.chomp
|
|
290
|
+
if stripped.match?(LinksProjection::ENTRY_LINE_RE) || stripped == LinksProjection::EMPTY_COMMENT
|
|
291
|
+
"\n"
|
|
292
|
+
else
|
|
293
|
+
line
|
|
294
|
+
end
|
|
295
|
+
end.join
|
|
296
|
+
end
|
|
297
|
+
|
|
264
298
|
def append_revision(dir, base, graph, files:, before_sources:, before_chain:)
|
|
265
299
|
path = File.join(dir, "revisions.md")
|
|
266
300
|
existing = File.exist?(path) ? File.read(path) : "# revisions.md\n\n"
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# encoding: UTF-8
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
# sweep-store-worktrees (intent 178, D2) -- ONE-TIME cleanup of the store
|
|
6
|
+
# worktrees Plastic used to provision under `~/.plastic/.worktrees/` before
|
|
7
|
+
# intent 178 retired the mechanism. Dry-run by default; nothing is ever
|
|
8
|
+
# removed without an explicit --apply, run only after reviewing the dry-run
|
|
9
|
+
# report (D2: "present a dry-run for owner review before applying").
|
|
10
|
+
#
|
|
11
|
+
# Usage:
|
|
12
|
+
# scripts/sweep-store-worktrees # dry run (default, safe)
|
|
13
|
+
# scripts/sweep-store-worktrees --apply # actually remove REMOVE candidates
|
|
14
|
+
# scripts/sweep-store-worktrees --home <dir> # override plastic_home (tests/CI only)
|
|
15
|
+
|
|
16
|
+
require_relative "lib/worktree_sweep"
|
|
17
|
+
|
|
18
|
+
def parse_args(argv)
|
|
19
|
+
opts = { apply: false, home: Dir.home }
|
|
20
|
+
i = 0
|
|
21
|
+
while i < argv.length
|
|
22
|
+
case argv[i]
|
|
23
|
+
when "--apply" then opts[:apply] = true
|
|
24
|
+
when "--home" then opts[:home] = argv[i += 1]
|
|
25
|
+
else
|
|
26
|
+
warn "sweep-store-worktrees: unknown argument #{argv[i].inspect}"
|
|
27
|
+
exit 1
|
|
28
|
+
end
|
|
29
|
+
i += 1
|
|
30
|
+
end
|
|
31
|
+
opts
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def main(argv)
|
|
35
|
+
opts = parse_args(argv)
|
|
36
|
+
plastic_home = File.expand_path(File.join(opts[:home], ".plastic"))
|
|
37
|
+
unless Dir.exist?(File.join(plastic_home, ".worktrees"))
|
|
38
|
+
puts "No #{File.join(plastic_home, '.worktrees')} directory; nothing to sweep."
|
|
39
|
+
exit 0
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
candidates = WorktreeSweep.candidates(plastic_home: plastic_home)
|
|
43
|
+
puts WorktreeSweep.dry_run_report(candidates)
|
|
44
|
+
|
|
45
|
+
if opts[:apply]
|
|
46
|
+
removed = WorktreeSweep.apply!(candidates, plastic_home: plastic_home)
|
|
47
|
+
puts ""
|
|
48
|
+
puts "Removed #{removed.length} worktree(s):"
|
|
49
|
+
removed.each { |c| puts " #{c.name}" }
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
main(ARGV) if $PROGRAM_NAME == __FILE__
|
|
@@ -44,12 +44,14 @@ deliberately); and the durable lock file is checked again after disarm, never me
|
|
|
44
44
|
trusted (exit 3 if it is somehow still present).
|
|
45
45
|
|
|
46
46
|
**Worktree cleanup (mandatory, intent 73c3).** `end-intent`'s step 5 calls
|
|
47
|
-
`Bridge.disarm_auto` by default, which calls `Worktree.release`, which removes
|
|
48
|
-
|
|
49
|
-
the
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
47
|
+
`Bridge.disarm_auto` by default, which calls `Worktree.release`, which removes the
|
|
48
|
+
intent's code worktree under `<repo>/.claude/worktrees/{id}--{slug}`, prunes the repo, and
|
|
49
|
+
clears the worktree block from the bridge. (Plastic used to also provision a paired store
|
|
50
|
+
worktree under `<plastic_home>/.worktrees/{id}--{slug}`; intent 178 retired it, since
|
|
51
|
+
lifecycle-doc writes go straight to the main store checkout, and intent 197's
|
|
52
|
+
branch-from-main plus scoped commit already gives them their own write safety.) This is the
|
|
53
|
+
plain remove path: the disarm route does NOT merge, so use it only when no release merges
|
|
54
|
+
the branch (the branch survives and can be reclaimed).
|
|
53
55
|
|
|
54
56
|
When the work is being shipped through a release, do NOT rely on this plain remove.
|
|
55
57
|
`skills/releasing/SKILL.md` reorders its own two steps for exactly this reason (intent 188,
|
package/skills/doctor/SKILL.md
CHANGED
|
@@ -106,6 +106,11 @@ If any checks have `fixable: true` AND status is not `pass`:
|
|
|
106
106
|
|
|
107
107
|
If no fixable issues exist, skip this step.
|
|
108
108
|
|
|
109
|
+
This "Fix all / Select individually / Skip" prompt IS the router the spec calls
|
|
110
|
+
`doctor --fix-all` (intent 197): doctor itself never mutates anything (see Step 5's table and
|
|
111
|
+
"Important Notes" below); "Fix all" means "dispatch every fixable finding to the maintenance
|
|
112
|
+
tool or skill that owns that class of repair," one row per fix_hint pattern.
|
|
113
|
+
|
|
109
114
|
### Step 5: Apply fixes
|
|
110
115
|
|
|
111
116
|
Use the `fix_hint` value to determine the correct action:
|
|
@@ -121,6 +126,7 @@ Use the `fix_hint` value to determine the correct action:
|
|
|
121
126
|
| "Run: provision-project-store {slug}" | Run `provision-project-store <slug>` (or invoke the `plastic-store-provisioning` skill) to create the missing store |
|
|
122
127
|
| "Re-run installer" | Run `npx -y @zalom/plastic@<channel> install --agent <agent>` (channel: -alpha->@alpha, -beta->@beta, else @latest) |
|
|
123
128
|
| "Dispatch plastic-store-curating ... revisions.md ..." | Invoke the `plastic-store-curating` (or the agent) to relocate the flagged section or ref into the intent's `revisions.md` via move-and-record (one dated, `[rule: <tag>]`-tagged entry per item), per PLASTIC.md > Structural maintenance and revisions.md. For a missing required section, restore or reproject it instead. |
|
|
129
|
+
| "Run scripts/project-links ... PRESERVES ... --drop-unbacked-links" | Run `ruby ~/.plastic/scripts/maintenance-run --tool project-links --intent <id> --apply` for the one flagged id (never run bare `project-links` against a real store outside the rare owner-approved batch exception, D2) |
|
|
124
130
|
|
|
125
131
|
For fixes the agent cannot handle automatically, explain what the user needs
|
|
126
132
|
to do manually. The `revisions.md` remedy is curator-applied (a move-and-record
|
|
@@ -247,11 +247,11 @@ lock correctly (G5): before intent 188 this path left the lock stranded, exactly
|
|
|
247
247
|
of bug closed by the End-tail enforcement work.
|
|
248
248
|
|
|
249
249
|
This is the release branch of `plastic-intent-ending`'s Step 5 disarm (`merge: true`), not a
|
|
250
|
-
separate concern: a release is the merge-then-remove path for the intent's
|
|
250
|
+
separate concern: a release is the merge-then-remove path for the intent's worktree (intent
|
|
251
251
|
73c3), so the intent's code branch is merged back into the default branch BEFORE the worktree
|
|
252
252
|
is removed. Drive it through `Worktree.finish` with `merge: true`, which merges the code
|
|
253
|
-
branch, then removes
|
|
254
|
-
|
|
253
|
+
branch, then removes the worktree, prunes the repo, and clears the worktree block from the
|
|
254
|
+
bridge:
|
|
255
255
|
|
|
256
256
|
```bash
|
|
257
257
|
ruby -r ~/.plastic/scripts/lib/worktree -r ~/.plastic/scripts/lib/bridge -e \
|
|
@@ -45,3 +45,12 @@ When an intent reaches a terminal state, moved to Completed OR Abandoned, do the
|
|
|
45
45
|
2. Call `plastic-intent-ending` for the terminal-transition close (INDEX move, savepoint `Done` bookend, store commit, disarm, and the QMD reindex last): `ruby ~/.plastic/scripts/end-intent --store <store> --id <id> --disposition delivered|abandoned`, then follow that skill's own disarm and reindex steps. Never restate those one-liners here.
|
|
46
46
|
|
|
47
47
|
After the agent completes, report what changed.
|
|
48
|
+
|
|
49
|
+
## Maintenance dispatch (intent 197)
|
|
50
|
+
|
|
51
|
+
When invoked to fix a structural finding on an intent OTHER than one currently being delivered
|
|
52
|
+
(for example, from `/plastic-doctor`'s fix-all routing), the `plastic-intent-curator` agent
|
|
53
|
+
follows its own step 7: it detects (never acquires) the target's delivery lock, requires a
|
|
54
|
+
clean working tree, and performs the fix on a fresh branch merged back to main as one closed
|
|
55
|
+
operation, with an append-only `revisions.md` receipt in the same pass as the edit. See
|
|
56
|
+
`agents/plastic-intent-curator.md` for the exact mechanics.
|
|
@@ -17,6 +17,22 @@
|
|
|
17
17
|
"result": "pass"
|
|
18
18
|
}
|
|
19
19
|
]
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"id": 2,
|
|
23
|
+
"scope": "behavior",
|
|
24
|
+
"set": "validation",
|
|
25
|
+
"prompt": "The user says: intent 26 (Completed) has a stale ## Links comment that contradicts its own real chain edge; fix it. Intent 26 is NOT the intent currently being delivered by this session.",
|
|
26
|
+
"expected_output": "Before editing intent 26, checks its delivery lock freshness (plastic-lock status --intent-dir, reading lock_fresh) and defers if fresh; requires a clean store working tree; creates a fresh maintenance branch off main; makes the scoped edit AND appends a revisions.md receipt in the same pass, refusing the edit if the receipt cannot be written; stages only the changed paths (never git add -A); merges the branch back to main and deletes it before reporting done.",
|
|
27
|
+
"files": [],
|
|
28
|
+
"assertions": [
|
|
29
|
+
{
|
|
30
|
+
"type": "human",
|
|
31
|
+
"check": "agent file states detect-only lock check, clean-tree precheck, branch-and-merge-back, and refuse-without-receipt as an unconditional sequence for maintenance on a non-current intent",
|
|
32
|
+
"observed": "agents/plastic-intent-curator.md step 7 covers all five sub-steps (a-f) exactly as asked",
|
|
33
|
+
"result": "pass"
|
|
34
|
+
}
|
|
35
|
+
]
|
|
20
36
|
}
|
|
21
37
|
]
|
|
22
38
|
}
|