@zalom/plastic 1.8.0 → 1.9.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.md +75 -552
- package/README.md +3 -2
- package/agents/plastic-intent-curator.md +2 -2
- package/deprecations.yml +10 -2
- package/hooks/{links-gate → edit-gates} +1 -1
- package/hooks/hooks.json +4 -44
- package/hooks/power-tools +8 -0
- package/package.json +1 -2
- package/scripts/codex-hook +17 -22
- package/scripts/doctor.rb +85 -3
- package/scripts/hook-code-gate +13 -28
- package/scripts/hook-create-gate +10 -57
- package/scripts/hook-edit-gates +58 -0
- package/scripts/hook-links-gate +8 -31
- package/scripts/hook-lock-gate +13 -58
- package/scripts/hook-power-tools +38 -0
- package/scripts/hook-savepoint-pre +8 -11
- package/scripts/lib/bridge.rb +2 -2
- package/scripts/lib/edit_gates.rb +342 -0
- package/scripts/lib/hook_registry.rb +61 -29
- package/scripts/lib/installer_core.rb +99 -17
- package/scripts/lib/qmd_hook.rb +21 -41
- package/scripts/lib/qmd_sync.rb +0 -15
- package/scripts/lib/revisions_writer.rb +1 -1
- package/scripts/restore-intent-v1 +16 -11
- package/skills/auto/SKILL.md +10 -0
- package/skills/auto/evals/evals.json +2 -2
- package/skills/conventions/SKILL.md +31 -0
- package/skills/conventions/references/completion-and-done.md +40 -0
- package/skills/conventions/references/gates-and-enforcement.md +23 -0
- package/skills/conventions/references/knowledge-graph.md +47 -0
- package/skills/conventions/references/lifecycle-and-savepoints.md +11 -0
- package/skills/conventions/references/locks-and-worktrees.md +113 -0
- package/skills/conventions/references/maintenance-and-revisions.md +170 -0
- package/skills/conventions/references/roadmaps.md +44 -0
- package/skills/conventions/references/tiers-and-dispatch.md +134 -0
- package/skills/doctor/SKILL.md +9 -1
- package/skills/intent-brainstorming/SKILL.md +4 -0
- package/skills/intent-continuing/SKILL.md +4 -0
- package/skills/intent-creating/SKILL.md +7 -2
- package/skills/intent-ending/SKILL.md +4 -0
- package/skills/intent-executing/SKILL.md +10 -0
- package/skills/intent-linking/SKILL.md +4 -0
- package/skills/intent-locking/SKILL.md +4 -0
- package/skills/intent-planning/SKILL.md +7 -0
- package/skills/intent-savepoint/SKILL.md +4 -0
- package/skills/intent-speccing/SKILL.md +4 -0
- package/skills/intent-starting/SKILL.md +10 -0
- package/skills/project-creating/references/project-scaffolding.md +2 -2
- package/skills/roadmap/SKILL.md +4 -0
- package/skills/roadmap-continuing/SKILL.md +4 -0
- package/skills/skill-creating/SKILL.md +3 -0
- package/skills/skill-creating/references/defaults-first.md +23 -0
- package/skills/skill-creating/references/hooks.md +4 -1
- package/skills/store-curating/SKILL.md +8 -0
- package/skills/store-indexing/SKILL.md +8 -0
- package/skills/tutorial/references/track-2-auto.md +2 -3
- package/PLASTIC-reference.md +0 -138
- package/hooks/code-gate +0 -27
- package/hooks/create-gate +0 -3
- package/hooks/lock-gate +0 -21
- package/hooks/qmd-search +0 -8
- package/hooks/retrieval-gate +0 -10
- package/hooks/savepoint-pre +0 -10
- package/scripts/hook-qmd-search +0 -44
- package/scripts/hook-retrieval-gate +0 -148
- package/scripts/lib/retrieval_gate.rb +0 -211
|
@@ -32,17 +32,20 @@ class InstallerCore
|
|
|
32
32
|
# Regex matching exactly one managed section (BEGIN line .. END line), non-greedy.
|
|
33
33
|
CODEX_SECTION_RE = /^<!-- BEGIN PLASTIC INTEGRATION.*?-->\n.*?\n<!-- END PLASTIC INTEGRATION -->\n?/m
|
|
34
34
|
|
|
35
|
-
# Curated essentials plus a pointer to ~/.plastic/PLASTIC.md
|
|
36
|
-
# ~/.codex/AGENTS.md. Not a slice of PLASTIC.md
|
|
37
|
-
#
|
|
35
|
+
# Curated essentials plus a pointer to ~/.plastic/PLASTIC.md and the plastic-conventions
|
|
36
|
+
# skill, injected into ~/.codex/AGENTS.md. Not a slice of PLASTIC.md itself: AGENTS.md is
|
|
37
|
+
# a shared file Codex merges from multiple sources, so this block stays a small,
|
|
38
|
+
# hand-curated pointer rather than embedding the core wholesale, and it never drifts
|
|
39
|
+
# because it only ever points, never duplicates.
|
|
38
40
|
CODEX_AGENTS_MD_BODY = <<~MD.freeze
|
|
39
41
|
Plastic is installed for this agent. Plastic is intent-driven state management: all
|
|
40
42
|
work flows through an intent, moved through What, Why, How, then Exec. Do not jump
|
|
41
43
|
straight to code.
|
|
42
44
|
|
|
43
45
|
Standing rules:
|
|
44
|
-
-
|
|
45
|
-
|
|
46
|
+
- Core conventions live in ~/.plastic/PLASTIC.md. Read it and follow it exactly. For
|
|
47
|
+
depth, read a chapter from ~/.agents/skills/plastic-conventions/references/ on demand.
|
|
48
|
+
Both are generated and overwritten on Plastic updates, so never edit them.
|
|
46
49
|
- Operational procedures are installed as skills under ~/.agents/skills/ (each
|
|
47
50
|
plastic-<name>/SKILL.md). Invoke one explicitly as $plastic-<name> (for example
|
|
48
51
|
$plastic-doctor), or let Codex pick one implicitly by matching its description.
|
|
@@ -232,6 +235,13 @@ class InstallerCore
|
|
|
232
235
|
def distribute(mode)
|
|
233
236
|
puts " \u{1f4e6} #{mode == :update ? "Updating" : "Installing"} core files to #{plastic_home}"
|
|
234
237
|
|
|
238
|
+
manifest_path = File.join(plastic_home, "manifest.json")
|
|
239
|
+
# Capture the prior manifest before the copy loop touches anything, so a
|
|
240
|
+
# file dropped from core_files (renamed/removed) can be pruned below
|
|
241
|
+
# instead of orphaning forever. Mirrors install_for_agent's prune (D14
|
|
242
|
+
# Why-gate correction: this global path never had it).
|
|
243
|
+
old_files = manifest_files(manifest_path)
|
|
244
|
+
|
|
235
245
|
FileUtils.mkdir_p(plastic_home)
|
|
236
246
|
FileUtils.mkdir_p(File.join(plastic_home, "scripts", "lib"))
|
|
237
247
|
FileUtils.mkdir_p(File.join(plastic_home, "templates"))
|
|
@@ -252,9 +262,11 @@ class InstallerCore
|
|
|
252
262
|
global_files = core_files.values.map { |d| File.join(plastic_home, d) }
|
|
253
263
|
global_files << File.join(plastic_home, "VERSION")
|
|
254
264
|
global_files = global_files.select { |p| File.exist?(p) }
|
|
255
|
-
write_manifest(global_files,
|
|
265
|
+
write_manifest(global_files, manifest_path)
|
|
266
|
+
|
|
267
|
+
pruned = prune_removed_files(old_files - global_files, root: plastic_home)
|
|
256
268
|
|
|
257
|
-
puts " \u{2705} Core files synced (v#{version})"
|
|
269
|
+
puts " \u{2705} Core files synced (v#{version})#{pruned.positive? ? ", #{pruned} stale file(s) pruned" : ""}"
|
|
258
270
|
end
|
|
259
271
|
|
|
260
272
|
# Templates ship in full: every file under templates/ in the repo must reach
|
|
@@ -282,7 +294,6 @@ class InstallerCore
|
|
|
282
294
|
def hand_registered_files
|
|
283
295
|
{
|
|
284
296
|
"PLASTIC.md" => "PLASTIC.md",
|
|
285
|
-
"PLASTIC-reference.md" => "PLASTIC-reference.md",
|
|
286
297
|
"deprecations.yml" => "deprecations.yml",
|
|
287
298
|
"config_asks.yml" => "config_asks.yml",
|
|
288
299
|
"scripts/folgezettel-id" => "scripts/folgezettel-id",
|
|
@@ -294,7 +305,7 @@ class InstallerCore
|
|
|
294
305
|
"scripts/hook-future-intent-check" => "scripts/hook-future-intent-check",
|
|
295
306
|
"scripts/hook-gate-check" => "scripts/hook-gate-check",
|
|
296
307
|
"scripts/hook-savepoint-pre" => "scripts/hook-savepoint-pre",
|
|
297
|
-
"scripts/hook-
|
|
308
|
+
"scripts/hook-power-tools" => "scripts/hook-power-tools",
|
|
298
309
|
"scripts/lib/qmd_hook.rb" => "scripts/lib/qmd_hook.rb",
|
|
299
310
|
"scripts/lib/power_tools.rb" => "scripts/lib/power_tools.rb",
|
|
300
311
|
"scripts/lib/agent_models.rb" => "scripts/lib/agent_models.rb",
|
|
@@ -303,8 +314,6 @@ class InstallerCore
|
|
|
303
314
|
"scripts/hook-code-gate" => "scripts/hook-code-gate",
|
|
304
315
|
"scripts/hook-lock-gate" => "scripts/hook-lock-gate",
|
|
305
316
|
"scripts/hook-bash-gate" => "scripts/hook-bash-gate",
|
|
306
|
-
"scripts/hook-retrieval-gate" => "scripts/hook-retrieval-gate",
|
|
307
|
-
"scripts/lib/retrieval_gate.rb" => "scripts/lib/retrieval_gate.rb",
|
|
308
317
|
"scripts/hook-auto-arm" => "scripts/hook-auto-arm",
|
|
309
318
|
"scripts/lib/bridge.rb" => "scripts/lib/bridge.rb",
|
|
310
319
|
"scripts/lib/lock.rb" => "scripts/lib/lock.rb",
|
|
@@ -345,6 +354,8 @@ class InstallerCore
|
|
|
345
354
|
"scripts/hook-create-gate" => "scripts/hook-create-gate",
|
|
346
355
|
"scripts/hook-links-gate" => "scripts/hook-links-gate",
|
|
347
356
|
"scripts/lib/links_gate.rb" => "scripts/lib/links_gate.rb",
|
|
357
|
+
"scripts/lib/edit_gates.rb" => "scripts/lib/edit_gates.rb",
|
|
358
|
+
"scripts/hook-edit-gates" => "scripts/hook-edit-gates",
|
|
348
359
|
"scripts/lib/apply_patch_envelope.rb" => "scripts/lib/apply_patch_envelope.rb",
|
|
349
360
|
"scripts/codex-hook" => "scripts/codex-hook",
|
|
350
361
|
"scripts/spawn-preamble" => "scripts/spawn-preamble",
|
|
@@ -403,10 +414,11 @@ class InstallerCore
|
|
|
403
414
|
MD
|
|
404
415
|
|
|
405
416
|
write_if_missing(File.join(plastic_home, "AGENTS.md"), <<~MD)
|
|
406
|
-
# Plastic
|
|
417
|
+
# Plastic: Agent Instructions
|
|
407
418
|
|
|
408
|
-
Read `PLASTIC.md` in this directory
|
|
409
|
-
Follow it exactly. Never modify it
|
|
419
|
+
Read `PLASTIC.md` in this directory for the core conventions; deeper doctrine lives
|
|
420
|
+
in the `plastic-conventions` skill's chapters. Follow it exactly. Never modify it:
|
|
421
|
+
it is overwritten on plugin updates.
|
|
410
422
|
|
|
411
423
|
This file (`AGENTS.md`) is where project-specific rules live.
|
|
412
424
|
|
|
@@ -508,7 +520,8 @@ class InstallerCore
|
|
|
508
520
|
end
|
|
509
521
|
|
|
510
522
|
new_files = manifest_files(manifest_path_for(key, config))
|
|
511
|
-
pruned = prune_removed_files(old_files - new_files
|
|
523
|
+
pruned = prune_removed_files(old_files - new_files,
|
|
524
|
+
root: [config[:dir], config[:home_dir], *shared_fragment_prune_roots])
|
|
512
525
|
result[:pruned] = pruned if pruned.positive?
|
|
513
526
|
|
|
514
527
|
# The legacy manifest is fully superseded once the new one is written; delete it so
|
|
@@ -588,7 +601,8 @@ class InstallerCore
|
|
|
588
601
|
restore_agent(config)
|
|
589
602
|
result = { agent: config[:name], success: false, reason: "verify failed - restored prior snapshot" }
|
|
590
603
|
else
|
|
591
|
-
prune_removed_files(manifest_files(manifest_path_for(key, config))
|
|
604
|
+
prune_removed_files(manifest_files(manifest_path_for(key, config)),
|
|
605
|
+
root: [config[:dir], config[:home_dir], *shared_fragment_prune_roots])
|
|
592
606
|
result = { agent: config[:name], success: false, reason: "verify failed - partial install pruned" }
|
|
593
607
|
end
|
|
594
608
|
end
|
|
@@ -599,10 +613,30 @@ class InstallerCore
|
|
|
599
613
|
|
|
600
614
|
# Delete tracked files present in the old manifest but absent from the new one,
|
|
601
615
|
# then remove any now-empty skill directories they lived in.
|
|
602
|
-
|
|
616
|
+
#
|
|
617
|
+
# `root:` is a mandatory containment boundary (intent 223 F5): every manifest path
|
|
618
|
+
# is read back from JSON written by write_manifest and could, on a hand-edited or
|
|
619
|
+
# otherwise corrupt manifest, name a path outside the install this call is pruning
|
|
620
|
+
# (for example `distribute` prunes against `~/.plastic/`, `install_for_agent` prunes
|
|
621
|
+
# against one agent's own directories). Accepts a single path or an array (an agent
|
|
622
|
+
# with a two-root schema, like Codex's `dir` plus `home_dir`, tracks files under
|
|
623
|
+
# both, so a single boundary would wrongly reject legitimate prunes under the root
|
|
624
|
+
# not passed). A candidate that resolves outside every given root is skipped and
|
|
625
|
+
# never deleted, so a stray manifest entry can never delete a path outside the
|
|
626
|
+
# target home.
|
|
627
|
+
def prune_removed_files(stale_files, root:)
|
|
628
|
+
expanded_roots = Array(root).compact.map { |r| File.expand_path(r) }
|
|
603
629
|
removed = 0
|
|
604
630
|
dirs = []
|
|
605
631
|
stale_files.each do |f|
|
|
632
|
+
if path_contained?(f, store_roots)
|
|
633
|
+
warn " \u{26a0}\u{fe0f} Refused to prune #{f}: inside the intent store"
|
|
634
|
+
next
|
|
635
|
+
end
|
|
636
|
+
unless path_contained?(f, expanded_roots)
|
|
637
|
+
warn " \u{26a0}\u{fe0f} Skipped prune of #{f}: outside #{expanded_roots.join(", ")}"
|
|
638
|
+
next
|
|
639
|
+
end
|
|
606
640
|
if File.exist?(f)
|
|
607
641
|
File.delete(f)
|
|
608
642
|
removed += 1
|
|
@@ -615,6 +649,54 @@ class InstallerCore
|
|
|
615
649
|
removed
|
|
616
650
|
end
|
|
617
651
|
|
|
652
|
+
# True when `path`, once expanded, is one of `roots` itself or lives underneath
|
|
653
|
+
# one of them. String-prefix containment guarded by a trailing separator so a
|
|
654
|
+
# sibling directory that merely shares a prefix (`/home/x-evil` vs root `/home/x`)
|
|
655
|
+
# never counts as contained.
|
|
656
|
+
def path_contained?(path, roots)
|
|
657
|
+
expanded = File.expand_path(path)
|
|
658
|
+
Array(roots).any? { |root| expanded == root || expanded.start_with?(root + File::SEPARATOR) }
|
|
659
|
+
end
|
|
660
|
+
|
|
661
|
+
# Intent 223 (post-delivery hardening): the user's entire intent history lives under
|
|
662
|
+
# `plastic_home/store` (the global store) and `plastic_home/projects` (every project
|
|
663
|
+
# store). `distribute` legitimately prunes with `root: plastic_home`, which CONTAINS
|
|
664
|
+
# both directories, so the ordinary `root:` containment check in `prune_removed_files`
|
|
665
|
+
# admits a store path instead of refusing it. Today that branch is unreachable (the
|
|
666
|
+
# pruned set is always `old_files - global_files`, both derived from `core_files` plus
|
|
667
|
+
# `VERSION`, so no store path can ever appear there), so this is defense in depth for
|
|
668
|
+
# an unreachable case, not a fix for a live bug. It exists because the standing rule is
|
|
669
|
+
# that NO path ever deletes the global or project intent stores, and the cost of being
|
|
670
|
+
# wrong (irreversible loss of the user's whole intent history) warrants a guard that
|
|
671
|
+
# fails milder than the bug it prevents, independent of what any caller's `root:` is or
|
|
672
|
+
# what a hand-edited manifest claims.
|
|
673
|
+
def store_roots
|
|
674
|
+
[File.join(plastic_home, "store"), File.join(plastic_home, "projects")].map { |p| File.expand_path(p) }
|
|
675
|
+
end
|
|
676
|
+
|
|
677
|
+
# `install_skills_flat` relocates any top-level underscore-prefixed markdown
|
|
678
|
+
# fragment (today: `_active-intent-gate.md`, `_decision-tables.md`) out of the
|
|
679
|
+
# per-agent skills tree and into `plastic_home` directly, then lists them in
|
|
680
|
+
# `installed`, so they are manifest-tracked and eligible for prune. But
|
|
681
|
+
# `install_for_agent`'s prune roots are `[config[:dir], config[:home_dir]]`,
|
|
682
|
+
# which do not cover `plastic_home`, so those two files could never actually be
|
|
683
|
+
# pruned (every attempt printed "Skipped prune of ... outside ..." to stderr).
|
|
684
|
+
#
|
|
685
|
+
# Fix scope (intent 223 N3): add each shared fragment's exact FILE path as its
|
|
686
|
+
# own containment root, never `plastic_home` itself as a directory root.
|
|
687
|
+
# `path_contained?` only matches a candidate that equals a root exactly or
|
|
688
|
+
# lives under `root + separator`; a root that is itself a file path can never
|
|
689
|
+
# have children, so listing these exact files as roots permits pruning ONLY
|
|
690
|
+
# those files, never anything else under `plastic_home`. This keeps
|
|
691
|
+
# `~/.plastic/store` and `~/.plastic/projects` (the user's intent stores, never
|
|
692
|
+
# prunable by standing rule) outside every prune root.
|
|
693
|
+
def shared_fragment_prune_roots
|
|
694
|
+
skills_source = File.join(package_root, "skills")
|
|
695
|
+
return [] unless File.directory?(skills_source)
|
|
696
|
+
Dir.children(skills_source).select { |e| e.start_with?("_") && e.end_with?(".md") }
|
|
697
|
+
.map { |e| File.join(plastic_home, e) }
|
|
698
|
+
end
|
|
699
|
+
|
|
618
700
|
def install_claude(config, force, argv: [], input: $stdin, reinstall: false)
|
|
619
701
|
hooks_dir = File.join(config[:dir], "hooks")
|
|
620
702
|
skills_root = File.join(config[:dir], "skills")
|
package/scripts/lib/qmd_hook.rb
CHANGED
|
@@ -5,22 +5,28 @@ require_relative "qmd_sync"
|
|
|
5
5
|
require_relative "power_tools"
|
|
6
6
|
|
|
7
7
|
# QmdHook - decision logic for the power-tools UserPromptSubmit hook (intents 66,
|
|
8
|
-
# 66b). Pure and dependency-injected: returns the additionalContext
|
|
9
|
-
# emit, or nil to emit nothing. The executable hook wires real deps and
|
|
10
|
-
# this is unit-tested with
|
|
8
|
+
# 66b, 187, 246). Pure and dependency-injected: returns the additionalContext
|
|
9
|
+
# string to emit, or nil to emit nothing. The executable hook wires real deps and
|
|
10
|
+
# prints; this is unit-tested with fake detectors (no real qmd, no network, no
|
|
11
|
+
# subprocess at all).
|
|
11
12
|
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
13
|
+
# It emits exactly one thing: the PowerTools mandate, one recommendation line per
|
|
14
|
+
# present tool (qmd for finding intents, Enola-first for code navigation, falling
|
|
15
|
+
# back to Serena; intent 187 added the enola_detector alongside the pre-existing
|
|
16
|
+
# serena_detector).
|
|
17
|
+
#
|
|
18
|
+
# Intent 246 removed the scored qmd hit injection this hook used to prepend.
|
|
19
|
+
# Intent 225 measured that injection at 0.24 intent-level recall@3 against a plain
|
|
20
|
+
# ripgrep control at 0.18, while agent-driven `qmd query` scored 0.71. The failure
|
|
21
|
+
# was recall, not latency, so caching and async were both rejected. QmdSync.search
|
|
22
|
+
# is untouched and still backs the `scripts/qmd-sync search` CLI verb. All three
|
|
23
|
+
# detectors are PATH and marker-file walks with no subprocess, which is why what is
|
|
24
|
+
# left costs about a tenth of a second.
|
|
16
25
|
module QmdHook
|
|
17
26
|
module_function
|
|
18
27
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def run(prompt:, cwd:, plastic_home:, runner: QmdSync.default_runner,
|
|
22
|
-
detector: QmdSync.method(:detect), limit: 3, min_score: 0.5,
|
|
23
|
-
serena_detector: nil, enola_detector: nil)
|
|
28
|
+
def run(cwd:, detector: QmdSync.method(:detect), serena_detector: nil,
|
|
29
|
+
enola_detector: nil)
|
|
24
30
|
serena_detector ||= -> { PowerTools.serena?(cwd: cwd) }
|
|
25
31
|
enola_detector ||= -> { PowerTools.enola?(cwd: cwd) }
|
|
26
32
|
qmd_present = !!detector.call
|
|
@@ -28,34 +34,8 @@ module QmdHook
|
|
|
28
34
|
enola_present = !!enola_detector.call
|
|
29
35
|
return nil unless qmd_present || serena_present || enola_present
|
|
30
36
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
# absent). The mandate itself is always-on for whichever tools are present.
|
|
35
|
-
search_ok = qmd_present && p.length >= MIN_PROMPT_LENGTH && p.downcase != "continue"
|
|
36
|
-
|
|
37
|
-
parts = []
|
|
38
|
-
if search_ok
|
|
39
|
-
collections = QmdSync.collections_for_cwd(cwd, plastic_home: plastic_home)
|
|
40
|
-
hits = QmdSync.search(p, collections: collections, limit: limit,
|
|
41
|
-
min_score: min_score, runner: runner, detector: detector)
|
|
42
|
-
if hits.any?
|
|
43
|
-
parts << "Related / prior Plastic intents (qmd BM25, includes completed) - " \
|
|
44
|
-
"check before treating this as new work:"
|
|
45
|
-
hits.each do |h|
|
|
46
|
-
loc = h[:file].to_s.sub(%r{\Aqmd://}, "")
|
|
47
|
-
pct = (h[:score] * 100).round
|
|
48
|
-
parts << "- [#{pct}%] #{loc} - #{h[:title]}"
|
|
49
|
-
end
|
|
50
|
-
parts << ""
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
mandate = PowerTools.mandate(cwd: cwd, qmd_detector: -> { qmd_present },
|
|
55
|
-
serena_detector: -> { serena_present },
|
|
56
|
-
enola_detector: -> { enola_present })
|
|
57
|
-
parts << mandate if mandate
|
|
58
|
-
return nil if parts.empty?
|
|
59
|
-
parts.join("\n")
|
|
37
|
+
PowerTools.mandate(cwd: cwd, qmd_detector: -> { qmd_present },
|
|
38
|
+
serena_detector: -> { serena_present },
|
|
39
|
+
enola_detector: -> { enola_present })
|
|
60
40
|
end
|
|
61
41
|
end
|
package/scripts/lib/qmd_sync.rb
CHANGED
|
@@ -134,21 +134,6 @@ module QmdSync
|
|
|
134
134
|
pid
|
|
135
135
|
end
|
|
136
136
|
|
|
137
|
-
# True when the QMD index has no pending (unembedded) documents. Binary
|
|
138
|
-
# freshness signal for the retrieval gate (intent 84, Lever 2). `qmd status` is
|
|
139
|
-
# plain text (no --json); it prints a line like "Pending: N need embedding".
|
|
140
|
-
# No pending line found -> treat as fresh (conservative: a parse miss must not
|
|
141
|
-
# block reads). Runner failure -> false (cannot confirm freshness). The caller
|
|
142
|
-
# gates on `detect` first, so absence is handled upstream; this only answers
|
|
143
|
-
# "is the present index fresh?". Pure via the injected runner.
|
|
144
|
-
def self.fresh?(runner: default_runner)
|
|
145
|
-
out, ok = runner.call(["status"])
|
|
146
|
-
return false unless ok && out
|
|
147
|
-
m = out[/^\s*Pending:\s*(\d+)\b/i, 1]
|
|
148
|
-
pending = m ? m.to_i : 0
|
|
149
|
-
pending.zero?
|
|
150
|
-
end
|
|
151
|
-
|
|
152
137
|
# Read-only status used by doctor and the session-start report line.
|
|
153
138
|
# Returns a structured hash; never mutates the index.
|
|
154
139
|
def status(plastic_home:, runner: default_runner, detector: method(:detect))
|
|
@@ -15,7 +15,7 @@ module RevisionsWriter
|
|
|
15
15
|
module_function
|
|
16
16
|
|
|
17
17
|
# PURE. Renders one `## Revision vN - TIMESTAMP` entry in the documented shape
|
|
18
|
-
# (
|
|
18
|
+
# (plastic-conventions > references/maintenance-and-revisions.md; templates/revisions.md).
|
|
19
19
|
# `n` is the next revision number (caller resolves it via next_revision_number, or a caller
|
|
20
20
|
# that already knows it, e.g. a batch writer amortizing one file read across many entries).
|
|
21
21
|
# `why` is the one-sentence reason ending in "[rule: <tag>]" (tag is appended here if the
|
|
@@ -6,9 +6,10 @@
|
|
|
6
6
|
# while preserving its frontmatter GRAPH (sources/chain) as a target-resolved
|
|
7
7
|
# union of the v1 snapshot and the current snapshot (intent 193). This is the
|
|
8
8
|
# ONLY sanctioned way to restore a completed intent to v1; a hand-run whole-file
|
|
9
|
-
# `git checkout`/revert is forbidden (see
|
|
10
|
-
# because it cannot tell prose from graph
|
|
11
|
-
# backlinks written after v1 (the 124/131 incident this tool
|
|
9
|
+
# `git checkout`/revert is forbidden (see plastic-conventions > references/maintenance-and-revisions.md,
|
|
10
|
+
# WORK vs MAINTENANCE and its Restore-to-v1 paragraph), because it cannot tell prose from graph
|
|
11
|
+
# metadata and silently destroys backlinks written after v1 (the 124/131 incident this tool
|
|
12
|
+
# exists to prevent).
|
|
12
13
|
#
|
|
13
14
|
# Usage:
|
|
14
15
|
# restore-intent-v1 <intent-id> --at <git-ref> [--plastic-home PATH] [--apply] \
|
|
@@ -19,12 +20,14 @@
|
|
|
19
20
|
# class of tool already destroyed live store data once. --apply is required to
|
|
20
21
|
# write.
|
|
21
22
|
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
# lock
|
|
26
|
-
#
|
|
27
|
-
#
|
|
23
|
+
# There is exactly one lock in Plastic, `delivery.lock`, and MAINTENANCE (this tool included)
|
|
24
|
+
# never acquires or checks it: WORK vs MAINTENANCE (plastic-conventions >
|
|
25
|
+
# references/maintenance-and-revisions.md) holds the corrected doctrine after intent 112's
|
|
26
|
+
# proposed second "maintenance.lock" was abandoned before merge and never shipped. This tool does
|
|
27
|
+
# NOT acquire, check, or manage `delivery.lock` itself (fail-open doctrine, intent 111: lock
|
|
28
|
+
# management is the orchestrator's job, never built into a CLI as a trap); it only prints a
|
|
29
|
+
# one-line reminder on --apply that the target intent must be terminal, which the caller is
|
|
30
|
+
# responsible for confirming before running with --apply.
|
|
28
31
|
#
|
|
29
32
|
# LINKS BLAST RADIUS: an applied restore reprojects `## Links` via
|
|
30
33
|
# scripts/project-links, which is a STORE-WIDE operation with no per-intent
|
|
@@ -142,8 +145,10 @@ class RestoreIntentV1CLI
|
|
|
142
145
|
File.write(md_path, new_md) if md_changed
|
|
143
146
|
other_files.each { |f, content| File.write(File.join(dir, f), content) }
|
|
144
147
|
|
|
145
|
-
puts "Reminder: restore-to-v1
|
|
146
|
-
"
|
|
148
|
+
puts "Reminder: restore-to-v1 is MAINTENANCE, not WORK (plastic-conventions > " \
|
|
149
|
+
"references/maintenance-and-revisions.md, WORK vs MAINTENANCE). There is no " \
|
|
150
|
+
"maintenance lock to hold; confirm the target intent is terminal (Completed or " \
|
|
151
|
+
"Abandoned) with no fresh delivery.lock before this --apply."
|
|
147
152
|
|
|
148
153
|
handle_links_reprojection(base)
|
|
149
154
|
append_revision(dir, base, graph, files: prose_changes,
|
package/skills/auto/SKILL.md
CHANGED
|
@@ -85,6 +85,10 @@ and artifact depth to that size. Extended walkthrough: `references/tiers.md`.
|
|
|
85
85
|
5. **Tier record.** `Tier: S|M|L` at the top of spec.md. Convention-only: read by the
|
|
86
86
|
orchestrator, never validated by any gate or by doctor.
|
|
87
87
|
|
|
88
|
+
Read `../plastic-conventions/references/tiers-and-dispatch.md` for tier sizing, agent-model
|
|
89
|
+
config, advisor routing, and the auto-mode human report contract behind the sizing above. This
|
|
90
|
+
path resolves relative to this skill's own installed directory.
|
|
91
|
+
|
|
88
92
|
## Arm the Lifecycle Gate (do this FIRST)
|
|
89
93
|
|
|
90
94
|
Immediately after selecting the intent - before any other work - arm auto mode. This
|
|
@@ -110,6 +114,9 @@ implies for later tool calls) read `references/end-tail.md`.
|
|
|
110
114
|
intent directory / `~/.plastic/`) until `plan.md` AND `checklist.md` exist for the intent.
|
|
111
115
|
Honor the cycle: What → Why (spec.md) → How (plan.md + actions/ + checklist.md) → Exec.
|
|
112
116
|
|
|
117
|
+
Read `../plastic-conventions/references/locks-and-worktrees.md` for delivery isolation: the
|
|
118
|
+
single-owner lock, claims, worktrees, solo mode, and the station ledger behind the arming above.
|
|
119
|
+
|
|
113
120
|
## Flags
|
|
114
121
|
|
|
115
122
|
- `--skip-permissions` - bypass hard stops on destructive actions on existing projects. Full trust mode. Default: off.
|
|
@@ -303,6 +310,9 @@ During initial project creation, all decisions are non-destructive by definition
|
|
|
303
310
|
|
|
304
311
|
## Completion
|
|
305
312
|
|
|
313
|
+
Read `../plastic-conventions/references/completion-and-done.md` for what "intent done" means and
|
|
314
|
+
the End-stage tail the steps below walk through.
|
|
315
|
+
|
|
306
316
|
1. Verify all checklist items are checked
|
|
307
317
|
2. Write `outcome.md` with detailed results, from `${CLAUDE_PLUGIN_ROOT}/templates/outcome.md`.
|
|
308
318
|
Set the frontmatter `disposition: delivered` (this is the delivered terminal). `outcome.md`
|
|
@@ -208,13 +208,13 @@
|
|
|
208
208
|
"scope": "behavior",
|
|
209
209
|
"set": "validation",
|
|
210
210
|
"prompt": "A power-tool is present (qmd on PATH, or a .serena marker / serena on PATH). A substantive prompt arrives in auto mode.",
|
|
211
|
-
"expected_output": "The UserPromptSubmit power-tools hook appends a MANDATORY obligation per present tool: a MUST-use-QMD line when qmd is present (to check for an existing or related intent before treating work as new), and a MUST-use-Serena line when serena is present (symbolic tools before grep/Read).
|
|
211
|
+
"expected_output": "The UserPromptSubmit power-tools hook appends a MANDATORY obligation per present tool: a MUST-use-QMD line when qmd is present (to check for an existing or related intent before treating work as new), and a MUST-use-Serena line when serena is present (symbolic tools before grep/Read). No search hits are injected: intent 246 removed the per-prompt qmd hit search, so the hook emits the mandate and nothing else.",
|
|
212
212
|
"files": [],
|
|
213
213
|
"assertions": [
|
|
214
214
|
{
|
|
215
215
|
"type": "code",
|
|
216
216
|
"check": "PowerTools.mandate returns MUST/MANDATORY lines for each present tool; QmdHook.run appends the mandate",
|
|
217
|
-
"observed": "power_tools_test.rb + qmd_hook_test.rb assert MUST wording; serena line gated on the serena detector",
|
|
217
|
+
"observed": "power_tools_test.rb + qmd_hook_test.rb assert MUST wording; serena line gated on the serena detector; qmd_hook_test.rb test_no_retrieval_hits_are_ever_injected asserts no hit header for any tool combination",
|
|
218
218
|
"result": "pass"
|
|
219
219
|
}
|
|
220
220
|
]
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: plastic-conventions
|
|
3
|
+
description: >
|
|
4
|
+
Chapters of Plastic doctrine used by more than one skill: the knowledge graph,
|
|
5
|
+
lifecycle and savepoints, tiers and dispatch, gates and enforcement, locks and
|
|
6
|
+
worktrees, completion, maintenance, and roadmaps. Read a chapter when its trigger
|
|
7
|
+
applies to the work in front of you.
|
|
8
|
+
user-invocable: false
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Plastic Conventions
|
|
12
|
+
|
|
13
|
+
The chapters below hold Plastic doctrine that more than one skill needs. Core always-on
|
|
14
|
+
conventions stay in `~/.plastic/PLASTIC.md`, which is injected at session start. Read a chapter
|
|
15
|
+
when the trigger in the second column applies to the work in front of you.
|
|
16
|
+
|
|
17
|
+
| Chapter | Read it when |
|
|
18
|
+
|---|---|
|
|
19
|
+
| `references/knowledge-graph.md` | when creating, linking, curating, or indexing intents and you need the sources-vs-chain doctrine, the tiers of influence, the `## Links` projection, or branch-vs-root directory semantics |
|
|
20
|
+
| `references/lifecycle-and-savepoints.md` | when running a lifecycle stage or a savepoint and you need the subagent report-home contract for how an insight reaches the intent |
|
|
21
|
+
| `references/tiers-and-dispatch.md` | when sizing an intent, choosing agent models, routing to the advisor, or writing an auto-mode human report |
|
|
22
|
+
| `references/gates-and-enforcement.md` | when a transition gate blocks you, or before using an audited escape, for the gate mechanics and the logging contract |
|
|
23
|
+
| `references/locks-and-worktrees.md` | before taking or releasing a delivery lock, and when working with claims, worktrees, solo mode, or the station ledger |
|
|
24
|
+
| `references/completion-and-done.md` | when ending an intent, for what "intent done" means and the End-stage tail |
|
|
25
|
+
| `references/maintenance-and-revisions.md` | before any structural maintenance edit, for WORK vs MAINTENANCE, the `revisions.md` move-and-record contract, the violation-tag catalog, and the context-economy measurement buckets |
|
|
26
|
+
| `references/roadmaps.md` | when creating, ordering, closing, or consuming a roadmap, for the file format and the status-mirror rule |
|
|
27
|
+
|
|
28
|
+
Other skills read these chapters directly at
|
|
29
|
+
`../plastic-conventions/references/<chapter>.md`, resolved relative to their own installed
|
|
30
|
+
directory. All three harnesses install skills flat into one shared skills root through the same
|
|
31
|
+
`install_skills_flat` call, so that path is harness-independent.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Completion and Done
|
|
2
|
+
|
|
3
|
+
This chapter holds what "intent done" means and the End-stage tail.
|
|
4
|
+
|
|
5
|
+
#### What "intent done" means (intent 93)
|
|
6
|
+
|
|
7
|
+
Done is one law with three signals, and they must agree. INDEX `## Completed` /
|
|
8
|
+
`## Abandoned` is the single canonical terminal marker: it is the store-wide ledger a fresh
|
|
9
|
+
session reads first, so it wins on any conflict. `outcome.md` is the "deliverable exists"
|
|
10
|
+
signal, and the savepoint `Done delivered|abandoned` line is the audit echo. All three must
|
|
11
|
+
agree; when they disagree, INDEX is authoritative and `doctor` flags the mismatch (the
|
|
12
|
+
`done_signals` check: `outcome.md` real but still under `## Active`, or terminal without a
|
|
13
|
+
real `outcome.md`, or a terminal intent whose savepoint carries no `Done` line).
|
|
14
|
+
|
|
15
|
+
`outcome.md` is mandatory at every terminal transition, delivered and abandoned alike. It
|
|
16
|
+
self-declares its disposition through a `disposition: delivered|abandoned` frontmatter
|
|
17
|
+
header. The delivered path authors it with the result; the abandoned path authors it with
|
|
18
|
+
the abandonment reason and no longer leaves the scaffolded placeholder sentinel in place.
|
|
19
|
+
|
|
20
|
+
The canonical End tail runs in this order, and the QMD reindex is always LAST, after the
|
|
21
|
+
purge: `outcome.md -> INDEX terminal -> savepoint Done -> commit -> disarm (Worktree.release
|
|
22
|
+
-> Lock.release -> purge) -> QMD reindex`. Running the reindex last keeps the index from
|
|
23
|
+
ever referencing a bridge or lock that disarm is about to remove.
|
|
24
|
+
|
|
25
|
+
`scripts/end-intent` performs this order's disarm step (verify the code worktree is clean,
|
|
26
|
+
then merge/remove worktrees, then clear the lock) as its own step 5, mechanically, since
|
|
27
|
+
intent 188: a session no longer needs a separate one-liner for it, and the script's own
|
|
28
|
+
exit code (0) is the single fact a caller needs that the intent is closed AND its delivery
|
|
29
|
+
lock is gone. A pre-flight lock guard runs before anything is written (refuses a live
|
|
30
|
+
foreign session, reclaims a stale one with an audit line), and a dirty code worktree
|
|
31
|
+
refuses before removal rather than force-discarding uncommitted changes.
|
|
32
|
+
|
|
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 and no
|
|
35
|
+
purge can fire (108's lock-held keep-guard keeps the bridge while `delivery.lock` exists).
|
|
36
|
+
Once the lock is released the window closes: the bridge becomes purge-eligible and the
|
|
37
|
+
directory is frozen. A crash mid-tail is recovered by stale-lock reclaim plus finishing the
|
|
38
|
+
tail; `doctor` surfaces this as a "stalled completion" (terminal in INDEX but the lock is
|
|
39
|
+
still present or stale). Finishing the tail is FINISHING a completion, never a reactivation:
|
|
40
|
+
a done intent is never moved back to `## Active`.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Gates and Enforcement
|
|
2
|
+
|
|
3
|
+
This chapter holds the escape-and-logging depth for each transition gate.
|
|
4
|
+
|
|
5
|
+
#### The gates by name
|
|
6
|
+
|
|
7
|
+
Each gate guards one thing. All are hard:
|
|
8
|
+
|
|
9
|
+
- **create-gate** validates the proposed intent file at What write-time (Write, Edit, and MCP
|
|
10
|
+
edits), so a malformed or incomplete intent never lands.
|
|
11
|
+
- **gate-check** enforces lifecycle stage order (spec.md before plan.md, the plan triplet before
|
|
12
|
+
the checklist, all checklist items before outcome.md).
|
|
13
|
+
- **lock-gate** arbitrates ownership and claims: it admits only the intent's lock owner or a
|
|
14
|
+
registered delegate to write into an active intent directory, and every deny names the
|
|
15
|
+
resolving `plastic-lock` command.
|
|
16
|
+
- **bash-gate** intercepts a write attempted through a bash or interpreter one-liner (a heredoc, a
|
|
17
|
+
`>` redirect, a `ruby -e` or `python -c` write), so the same rules apply whether an edit goes
|
|
18
|
+
through the Write tool or a shell. A trailing `# plastic-ok` comment is an auditable escape that
|
|
19
|
+
lets a deliberate command through, and every use is logged to
|
|
20
|
+
`~/.plastic/.cache/gate-escapes.log`. The code gate (Write and Edit) carries the identical
|
|
21
|
+
audited `# plastic-ok` escape, logged to the same file. The escape does not extend to
|
|
22
|
+
`NotebookEdit` or MCP structural edits: they are still gated, just without an escape hatch.
|
|
23
|
+
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Knowledge Graph
|
|
2
|
+
|
|
3
|
+
This chapter holds the linking doctrine from Frontmatter and the branch-vs-root directory semantics from Directory Naming.
|
|
4
|
+
|
|
5
|
+
- `sources` (formative, must-load, acyclic) and `chain` (forward + relational, lighter,
|
|
6
|
+
may cycle) form the directed knowledge graph. Reciprocity is one-directional: every
|
|
7
|
+
`sources` edge has a reciprocal `chain` entry (I1), but `chain` may carry relational
|
|
8
|
+
entries with no reciprocal `sources` (I2), so the graph is not strictly symmetric.
|
|
9
|
+
|
|
10
|
+
- `## Links` (I5) is the human-readable projection of the graph. It mirrors the
|
|
11
|
+
frontmatter exactly: every entry is `- [[id--slug|<target's full intent: text>]]`, a
|
|
12
|
+
clickable `id--slug` wikilink target with the target intent's full `intent:` text as the
|
|
13
|
+
label (cross-store targets render `- [[store:id--slug|<target's full intent: text>]]`).
|
|
14
|
+
Ordering is mandatory: all `sources` first (top), then all `chain`, frontmatter order
|
|
15
|
+
preserved within each group. Sources never appear at the end. No source/chain tags, no
|
|
16
|
+
sub-grouping. An intent with empty `sources` and `chain` carries the empty-state comment.
|
|
17
|
+
- `## Links` is a DERIVED view, not a place to author links (Convention over Configuration).
|
|
18
|
+
It equals the projection of `sources` (first) then `chain`. Never hand-write or hand-edit a
|
|
19
|
+
`## Links` line, and never auto-delete one. The edge lives in the frontmatter graph; the
|
|
20
|
+
section is regenerated from it (doctor `graph_links_projection` enforces this identity). To
|
|
21
|
+
add a link, add the frontmatter edge, then reproject.
|
|
22
|
+
|
|
23
|
+
- Links are decided by CONTEXT INFLUENCE, not by shared files, shared symbols, or a topic
|
|
24
|
+
similarity score. The question is whether one intent's context actually informed another.
|
|
25
|
+
Three tiers:
|
|
26
|
+
- **sources:** the foundational context that shaped this intent's creation (a split, an idea
|
|
27
|
+
born during development, a merge). Earns an edge.
|
|
28
|
+
- **chain:** the context that materially helps DELIVER this intent. This is a HIGH bar: only
|
|
29
|
+
the genuinely delivery-moving intents, not everything in the same area. Earns an edge,
|
|
30
|
+
reflected in `## Links`.
|
|
31
|
+
- **tags:** a loose theme grouping for search. NOT a link. A shared tag is a door INTO the
|
|
32
|
+
store (filtered discovery), not a pathway BETWEEN two notes.
|
|
33
|
+
Judging influence is an agent's call, made by reading the candidate's Intent and Context. A
|
|
34
|
+
script cannot grade it, so `scripts/link-suggest` only gathers candidates with that evidence,
|
|
35
|
+
records a confirmed edge with a rating and reason, and flags drift.
|
|
36
|
+
|
|
37
|
+
**Branch vs root: the semantic decision.** The numbering is mechanics; choosing
|
|
38
|
+
*whether* to branch is meaning:
|
|
39
|
+
|
|
40
|
+
- **Branch (`14a`, `14b`):** a sub-task, refinement, or direct continuation of the
|
|
41
|
+
parent. It cannot stand on its own; it only makes sense as part of the parent's work.
|
|
42
|
+
- **Root (`15`, `16`)**: an independent thought, even if inspired by another intent.
|
|
43
|
+
Reserve `sources` for true created-from provenance (intents this was built out of). An
|
|
44
|
+
independent intent merely related to or inspired by another carries NO `sources`; record
|
|
45
|
+
the relation on the PREDECESSOR's `chain` (and mirror it as a
|
|
46
|
+
`[[id--slug|<target's full intent: text>]]` wikilink in `## Links`).
|
|
47
|
+
- **Rule of thumb:** if the intent could exist without its parent, it's a root.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Lifecycle and Savepoints
|
|
2
|
+
|
|
3
|
+
This chapter holds the subagent report-home depth for how an insight reaches the intent when the writer cannot write the file itself.
|
|
4
|
+
|
|
5
|
+
Background sessions and dispatched sub-agents do not write the insight themselves. They carry
|
|
6
|
+
each nugget home in the completion report's `insights:` field, and the orchestrator (or any
|
|
7
|
+
agent that can write the file) persists it via the helper. A session that cannot write the
|
|
8
|
+
intent file still returns its report, so the insight survives.
|
|
9
|
+
|
|
10
|
+
For the stage table (What/Why/How/Exec, deliverable, owning skill), see PLASTIC.md's Lifecycle
|
|
11
|
+
Stages section; each named skill's own `references/` holds that stage's own depth.
|