@zalom/plastic 1.12.0 → 1.14.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/hooks/statusline +300 -155
- package/package.json +1 -1
- package/scripts/doctor.rb +131 -9
- package/scripts/lib/bridge.rb +4 -3
- package/scripts/lib/doctor_core.rb +289 -35
- package/scripts/lib/doctor_exclusions.rb +54 -0
- package/scripts/lib/hook_registry.rb +20 -0
- package/scripts/lib/installer_core.rb +40 -7
- package/scripts/maintenance-run +124 -14
- package/scripts/scaffold-intent +2 -2
- package/skills/conventions/SKILL.md +1 -1
- package/skills/conventions/references/gates-and-enforcement.md +13 -8
- package/skills/conventions/references/maintenance-and-revisions.md +14 -0
- package/skills/doctor/SKILL.md +14 -0
- package/skills/skill-creating/SKILL.md +3 -0
- package/skills/skill-creating/references/skills.md +3 -0
package/scripts/doctor.rb
CHANGED
|
@@ -546,10 +546,16 @@ class Doctor
|
|
|
546
546
|
# :operational_gap when "savepoint_operational" is in that set; the :gap bucket (signals_complete,
|
|
547
547
|
# the outcome.md check) never consults it, which is what keeps the exclusion key (intent_id,
|
|
548
548
|
# rule) rather than just intent_id (see test/doctor_done_signals_test.rb case 12).
|
|
549
|
+
#
|
|
550
|
+
# `:excluded_rules_fired` (intent 280) names the rules that actually suppressed a finding for
|
|
551
|
+
# this dir - not merely the rules registered for it. The caller uses this to build the `consumed`
|
|
552
|
+
# set `DoctorExclusions.dead_rows` needs: a registered rule that never fires here (nothing to
|
|
553
|
+
# suppress) is exactly what makes the row dead.
|
|
549
554
|
def done_signal_findings_for_dir(dir, label:, scope:, dirname:, terminal:, active:, excluded_rules: [])
|
|
550
555
|
outcome = File.join(dir, "outcome.md")
|
|
551
556
|
outcome_real = Bridge.stage_file_present?(outcome)
|
|
552
|
-
findings = { conflict: nil, phantom: nil, gap: [], operational_gap: [], excluded: [],
|
|
557
|
+
findings = { conflict: nil, phantom: nil, gap: [], operational_gap: [], excluded: [],
|
|
558
|
+
excluded_rules_fired: [], stalled: nil }
|
|
553
559
|
|
|
554
560
|
# HARD conflict: the deliverable exists but INDEX still says Active. This
|
|
555
561
|
# is the one true INDEX-wins disagreement, so it stays a fail.
|
|
@@ -586,7 +592,8 @@ def done_signal_findings_for_dir(dir, label:, scope:, dirname:, terminal:, activ
|
|
|
586
592
|
# reconstructible via maintenance-run --tool rebuild-savepoint, so this is repairable and
|
|
587
593
|
# reported as a fixable warn (savepoint_operational).
|
|
588
594
|
savepoint = File.join(dir, "savepoint.md")
|
|
589
|
-
|
|
595
|
+
suppressed = excluded_rules.include?("savepoint_operational")
|
|
596
|
+
bucket = suppressed ? findings[:excluded] : findings[:operational_gap]
|
|
590
597
|
if !File.exist?(savepoint)
|
|
591
598
|
bucket << "#{label}: terminal in INDEX but savepoint.md is missing " \
|
|
592
599
|
"entirely (operational - reconstructible)"
|
|
@@ -594,6 +601,7 @@ def done_signal_findings_for_dir(dir, label:, scope:, dirname:, terminal:, activ
|
|
|
594
601
|
bucket << "#{label}: terminal in INDEX but savepoint.md has no " \
|
|
595
602
|
"`Done delivered|abandoned` line (operational - reconstructible)"
|
|
596
603
|
end
|
|
604
|
+
findings[:excluded_rules_fired] << "savepoint_operational" if suppressed && findings[:excluded].any?
|
|
597
605
|
|
|
598
606
|
# Stalled completion: unchanged, never consulted amnesty.
|
|
599
607
|
if File.exist?(Lock.path(dir))
|
|
@@ -614,6 +622,8 @@ def check_done_signals(scopes: nil)
|
|
|
614
622
|
exclusion_errors = [] # malformed doctor-exclusions lines, scope-tagged
|
|
615
623
|
exclusion_error_paths = []
|
|
616
624
|
exclusion_paths = [] # files that actually contributed a live exclusion
|
|
625
|
+
dead_rows = [] # exclusion rows that suppressed nothing this run (intent 280)
|
|
626
|
+
dead_row_paths = []
|
|
617
627
|
stalled = []
|
|
618
628
|
phantoms = []
|
|
619
629
|
|
|
@@ -624,6 +634,25 @@ def check_done_signals(scopes: nil)
|
|
|
624
634
|
exclusion_error_paths << exclusions[:path]
|
|
625
635
|
end
|
|
626
636
|
|
|
637
|
+
consumed = { "savepoint_operational" => [] }
|
|
638
|
+
# `known_ids` (post-review fix): every intent id with a REAL DIRECTORY in this store, scanned
|
|
639
|
+
# directly from disk - independent of INDEX.md. An id can have a directory on disk without
|
|
640
|
+
# being listed in INDEX (a de-indexed "ghost"), and the walk below alone would never visit it;
|
|
641
|
+
# deriving known_ids from walk membership misclassified that ghost as :no_intent (deleted)
|
|
642
|
+
# even though the directory plainly still exists.
|
|
643
|
+
# Shares `store_intent_dirs` (159, intent 189's store-discovery helper) rather than
|
|
644
|
+
# reimplementing the same directory scan (review fix): one predicate for "what is an intent
|
|
645
|
+
# directory in this store", never two that could drift apart.
|
|
646
|
+
known_ids = if File.directory?(store[:store_dir])
|
|
647
|
+
store_intent_dirs(store[:store_dir]).map { |e| e.split("--", 2).first }
|
|
648
|
+
else
|
|
649
|
+
[]
|
|
650
|
+
end
|
|
651
|
+
# `evaluated_ids`: the narrower set the walk below actually judges (INDEX-listed and on
|
|
652
|
+
# disk). An id with a real directory that this run never evaluated (on disk, unindexed)
|
|
653
|
+
# carries no evidence either way and must never be called dead - dead_rows leaves it out.
|
|
654
|
+
evaluated_ids = []
|
|
655
|
+
|
|
627
656
|
index_sections_by_dir(store[:index]).each do |dirname, in_sections|
|
|
628
657
|
dir = File.join(store[:store_dir], dirname)
|
|
629
658
|
next unless File.directory?(dir)
|
|
@@ -632,6 +661,7 @@ def check_done_signals(scopes: nil)
|
|
|
632
661
|
active = in_sections.include?("Active") && !terminal
|
|
633
662
|
label = "#{store[:scope]} store/#{dirname}"
|
|
634
663
|
intent_id = dirname.split("--", 2).first
|
|
664
|
+
evaluated_ids << intent_id
|
|
635
665
|
excluded_rules = DoctorExclusions.rules_for(exclusions, intent_id)
|
|
636
666
|
|
|
637
667
|
findings = done_signal_findings_for_dir(
|
|
@@ -646,11 +676,29 @@ def check_done_signals(scopes: nil)
|
|
|
646
676
|
excluded.concat(findings[:excluded])
|
|
647
677
|
exclusion_paths << exclusions[:path]
|
|
648
678
|
end
|
|
679
|
+
findings[:excluded_rules_fired].each { |fired| (consumed[fired] ||= []) << intent_id }
|
|
649
680
|
stalled << findings[:stalled] if findings[:stalled]
|
|
650
681
|
end
|
|
682
|
+
|
|
683
|
+
# Drift in the governance record itself (intent 280): rows naming a pair that produced no
|
|
684
|
+
# finding this run. Computed by set subtraction against the walk above, never re-derived from
|
|
685
|
+
# the exclusion file (208; the intent 200 self-diff). `:no_intent` below only ever fires when
|
|
686
|
+
# `known_ids` (a real directory scan) truly has no entry for the id - never merely because the
|
|
687
|
+
# walk did not visit it.
|
|
688
|
+
DoctorExclusions.dead_rows(exclusions, consumed: consumed, known_ids: known_ids,
|
|
689
|
+
evaluated_ids: evaluated_ids).each do |row|
|
|
690
|
+
reason = if row[:reason] == :no_intent
|
|
691
|
+
"names no live intent directory (a typo, or the intent was deleted)"
|
|
692
|
+
else
|
|
693
|
+
"names an intent with no current #{row[:rule]} finding"
|
|
694
|
+
end
|
|
695
|
+
dead_rows << "#{store[:scope]}: #{exclusions[:path]}: #{row[:rule]} #{row[:id]} - #{reason}"
|
|
696
|
+
dead_row_paths << exclusions[:path]
|
|
697
|
+
end
|
|
651
698
|
end
|
|
652
699
|
exclusion_paths.uniq!
|
|
653
700
|
exclusion_error_paths.uniq!
|
|
701
|
+
dead_row_paths.uniq!
|
|
654
702
|
|
|
655
703
|
checks = []
|
|
656
704
|
|
|
@@ -697,7 +745,15 @@ def check_done_signals(scopes: nil)
|
|
|
697
745
|
# a malformed exclusion file can never report pass (loud), a clean remaining gap set reports
|
|
698
746
|
# pass with the exclusion count folded in, and a real remaining gap set stays warn, same as
|
|
699
747
|
# before intent 274, with the same count folded in when exclusions applied.
|
|
748
|
+
#
|
|
749
|
+
# `dead_suffix` (intent 280) folds in a second, independent drift notice: exclusion rows that
|
|
750
|
+
# suppressed nothing this run. It is purely informational, exactly like `exclusion_suffix` - it
|
|
751
|
+
# never changes status on any of the three branches below, because a stale governance-record row
|
|
752
|
+
# is bookkeeping drift, not a store regression (219 D6 is untouched: no disposition is invented).
|
|
700
753
|
exclusion_suffix = excluded.empty? ? "" : " (#{excluded.size} excluded via #{exclusion_paths.join(", ")})"
|
|
754
|
+
dead_suffix = dead_rows.empty? ? "" : " (#{dead_rows.size} dead row#{dead_rows.size == 1 ? "" : "s"} " \
|
|
755
|
+
"in #{dead_row_paths.join(", ")}, suppressing nothing - prune with " \
|
|
756
|
+
"`maintenance-run --tool register-exclusions --prune`)"
|
|
701
757
|
|
|
702
758
|
if exclusion_errors.any?
|
|
703
759
|
checks << check(
|
|
@@ -705,8 +761,8 @@ def check_done_signals(scopes: nil)
|
|
|
705
761
|
message: "#{operational_gaps.size} terminal intent#{operational_gaps.size == 1 ? "" : "s"} " \
|
|
706
762
|
"missing an operational savepoint.md or its Done echo, and " \
|
|
707
763
|
"#{exclusion_errors.size} doctor-exclusions error#{exclusion_errors.size == 1 ? "" : "s"} " \
|
|
708
|
-
"(a malformed exclusion file never suppresses a finding)#{exclusion_suffix}",
|
|
709
|
-
details: operational_gaps + exclusion_errors, fixable: true,
|
|
764
|
+
"(a malformed exclusion file never suppresses a finding)#{exclusion_suffix}#{dead_suffix}",
|
|
765
|
+
details: operational_gaps + exclusion_errors + dead_rows, fixable: true,
|
|
710
766
|
fix_hint: "Fix the malformed doctor-exclusions file(s) (#{exclusion_error_paths.join(", ")}) - " \
|
|
711
767
|
"format `rule_name id id id`, blank lines and # comments ignored - then reconstruct " \
|
|
712
768
|
"any remaining real gap via `maintenance-run --tool rebuild-savepoint --intent <id> " \
|
|
@@ -716,14 +772,17 @@ def check_done_signals(scopes: nil)
|
|
|
716
772
|
elsif operational_gaps.empty?
|
|
717
773
|
checks << check(
|
|
718
774
|
category: "done_signals", name: "savepoint_operational", status: "pass",
|
|
719
|
-
message: "No terminal intent is missing an operational savepoint.md or its Done echo
|
|
775
|
+
message: "No terminal intent is missing an operational savepoint.md or its Done echo" \
|
|
776
|
+
"#{exclusion_suffix}#{dead_suffix}",
|
|
777
|
+
details: dead_rows
|
|
720
778
|
)
|
|
721
779
|
else
|
|
722
780
|
checks << check(
|
|
723
781
|
category: "done_signals", name: "savepoint_operational", status: "warn",
|
|
724
782
|
message: "#{operational_gaps.size} terminal intent#{operational_gaps.size == 1 ? "" : "s"} " \
|
|
725
|
-
"missing an operational savepoint.md or its Done echo (reconstructible)
|
|
726
|
-
|
|
783
|
+
"missing an operational savepoint.md or its Done echo (reconstructible)" \
|
|
784
|
+
"#{exclusion_suffix}#{dead_suffix}",
|
|
785
|
+
details: operational_gaps + dead_rows, fixable: true,
|
|
727
786
|
fix_hint: "Reconstruct the minimal two-line started/Done echo via " \
|
|
728
787
|
"`maintenance-run --tool rebuild-savepoint --intent <id> --apply` (197-conformant: " \
|
|
729
788
|
"receipt-before-write via RevisionsWriter, one intent per invocation, owner-approval-gated)."
|
|
@@ -881,12 +940,19 @@ end
|
|
|
881
940
|
)]
|
|
882
941
|
end
|
|
883
942
|
|
|
943
|
+
# The owning store's INDEX.md, resolved from `scope` through the memoized store_discovery
|
|
944
|
+
# (same {key:, index:} shape done_signal_stores enumerates). intent_savepoint_truthful_check
|
|
945
|
+
# needs it to reach that store's doctor-exclusions table and to ask INDEX whether this
|
|
946
|
+
# intent is terminal (intent 281 D3/D6). nil when the scope resolves to no known store,
|
|
947
|
+
# which restores the pre-281 behavior exactly.
|
|
948
|
+
index_path = store_discovery[:stores].find { |s| s[:key] == scope }&.fetch(:index, nil)
|
|
949
|
+
|
|
884
950
|
[
|
|
885
951
|
intent_structure_check(intent_dir),
|
|
886
952
|
intent_lifecycle_artifacts_check(intent_dir, disposition),
|
|
887
953
|
intent_checklist_complete_check(intent_dir),
|
|
888
954
|
intent_links_projection_check_for(id, scope),
|
|
889
|
-
intent_savepoint_truthful_check(intent_dir),
|
|
955
|
+
intent_savepoint_truthful_check(intent_dir, index_path: index_path),
|
|
890
956
|
]
|
|
891
957
|
end
|
|
892
958
|
|
|
@@ -997,11 +1063,67 @@ end
|
|
|
997
1063
|
end
|
|
998
1064
|
end
|
|
999
1065
|
|
|
1066
|
+
# Whether this one intent's missing-savepoint finding is knowingly excluded, for the
|
|
1067
|
+
# per-intent surface (intent 281). Returns {excluded:, errors:, path:}.
|
|
1068
|
+
#
|
|
1069
|
+
# Same rule id as the store-wide sweep, `savepoint_operational` (281 D1): the fact is
|
|
1070
|
+
# identical (a terminal intent with no savepoint.md), so one registration in one
|
|
1071
|
+
# doctor-exclusions file covers both surfaces and the owner never learns a second name for
|
|
1072
|
+
# one gap. RuleCatalog is deliberately NOT extended.
|
|
1073
|
+
#
|
|
1074
|
+
# Terminal-gated (281 D3): done_signal_findings_for_dir only ever produces this finding
|
|
1075
|
+
# inside `if terminal`, so honoring the exclusion for a still-Active intent would suppress a
|
|
1076
|
+
# strictly larger set of facts than the rule id names - and would let a mistyped id silence
|
|
1077
|
+
# the live, repairable warning scripts/end-intent's pre-write gate exists to raise.
|
|
1078
|
+
#
|
|
1079
|
+
# Never raises: DoctorExclusions is fail-open by contract (274 D5) and index_sections_by_dir
|
|
1080
|
+
# returns an empty map for a missing INDEX.
|
|
1081
|
+
def savepoint_exclusion_for(intent_dir, index_path)
|
|
1082
|
+
none = { excluded: false, errors: [], path: nil }
|
|
1083
|
+
return none unless index_path
|
|
1084
|
+
|
|
1085
|
+
dirname = File.basename(intent_dir)
|
|
1086
|
+
return none unless (index_sections_by_dir(index_path)[dirname] & ["Completed", "Abandoned"]).any?
|
|
1087
|
+
|
|
1088
|
+
loaded = DoctorExclusions.load(index_path)
|
|
1089
|
+
rules = DoctorExclusions.rules_for(loaded, dirname.split("--", 2).first)
|
|
1090
|
+
{ excluded: rules.include?("savepoint_operational"), errors: loaded[:errors], path: loaded[:path] }
|
|
1091
|
+
end
|
|
1092
|
+
|
|
1000
1093
|
# WARN-only, per intent 134 (savepoint truthfulness is advisory, never a hard gate). Do not
|
|
1001
1094
|
# change this to FAIL: it would silently contradict a standing, binding ruling.
|
|
1002
|
-
|
|
1095
|
+
#
|
|
1096
|
+
# `index_path:` (intent 281) is the owning store's INDEX.md, threaded from check_intent_end.
|
|
1097
|
+
# It makes this surface honor the same doctor-exclusions registration check_done_signals
|
|
1098
|
+
# already honors for the same fact, under the same rule id (281 D1). Only the missing-file
|
|
1099
|
+
# branch below is excludable: the phantom-line branch is permanently non-suppressible by id
|
|
1100
|
+
# or scope (intent 211, 281 D2). Omitting index_path restores the pre-281 behavior exactly.
|
|
1101
|
+
def intent_savepoint_truthful_check(intent_dir, index_path: nil)
|
|
1003
1102
|
savepoint = File.join(intent_dir, "savepoint.md")
|
|
1004
1103
|
unless File.exist?(savepoint)
|
|
1104
|
+
exclusion = savepoint_exclusion_for(intent_dir, index_path)
|
|
1105
|
+
|
|
1106
|
+
# A loader error never suppresses anything (274 D5: fail milder than the bug), and the
|
|
1107
|
+
# check that consulted the file is where the error is reported.
|
|
1108
|
+
if exclusion[:errors].any?
|
|
1109
|
+
return check(
|
|
1110
|
+
category: "intent_end", name: "intent_savepoint_truthful", status: "warn",
|
|
1111
|
+
message: "savepoint.md is missing, and #{exclusion[:errors].size} doctor-exclusions " \
|
|
1112
|
+
"error#{exclusion[:errors].size == 1 ? "" : "s"} " \
|
|
1113
|
+
"(a malformed exclusion file never suppresses a finding)",
|
|
1114
|
+
details: exclusion[:errors], fixable: true,
|
|
1115
|
+
fix_hint: "Fix the malformed doctor-exclusions file (#{exclusion[:path]}) - format " \
|
|
1116
|
+
"`rule_name id id id`, blank lines and # comments ignored - then re-run."
|
|
1117
|
+
)
|
|
1118
|
+
end
|
|
1119
|
+
|
|
1120
|
+
# Excluded: the fact stays in the message with the honest count and the file that caused
|
|
1121
|
+
# the suppression, and nothing lands in details (274 D4's wording, N is always 1 here).
|
|
1122
|
+
if exclusion[:excluded]
|
|
1123
|
+
return check(category: "intent_end", name: "intent_savepoint_truthful", status: "pass",
|
|
1124
|
+
message: "savepoint.md is missing (1 excluded via #{exclusion[:path]})")
|
|
1125
|
+
end
|
|
1126
|
+
|
|
1005
1127
|
return check(category: "intent_end", name: "intent_savepoint_truthful", status: "warn",
|
|
1006
1128
|
message: "savepoint.md is missing")
|
|
1007
1129
|
end
|
package/scripts/lib/bridge.rb
CHANGED
|
@@ -37,8 +37,9 @@ module Bridge
|
|
|
37
37
|
# `## Active` block); such bridges are purged. An Active intent's bridge is kept
|
|
38
38
|
# unconditionally, because while the intent is live the bridge is still load-
|
|
39
39
|
# bearing: it is the continuation signal (a parked or interrupted run resumes
|
|
40
|
-
# from it) and the anti-collision lock
|
|
41
|
-
#
|
|
40
|
+
# from it) and the anti-collision lock for parallel deliveries on one store,
|
|
41
|
+
# keeping each session's gate checks and locks from overwriting another's. An
|
|
42
|
+
# age window was the wrong
|
|
42
43
|
# axis: it left dead bridges resident for ~2 days AND could reap bridges of
|
|
43
44
|
# interrupted-but-still-active intents, which are exactly the ones to preserve.
|
|
44
45
|
|
|
@@ -121,7 +122,7 @@ module Bridge
|
|
|
121
122
|
# The CLAUDE_CODE_SESSION_ID fallback (intent 79) carries the bg/headless real
|
|
122
123
|
# session id (Claude Code passes session_id on stdin, not via an env var; the
|
|
123
124
|
# headless id lives in CLAUDE_CODE_SESSION_ID). Keying by the real id (instead of
|
|
124
|
-
# a derived hash) lets the
|
|
125
|
+
# a derived hash) lets the gate hooks, which receive that same id on stdin, find
|
|
125
126
|
# the bridge by direct filename lookup.
|
|
126
127
|
def self.resolve_session(explicit, intent_id:, store:)
|
|
127
128
|
return explicit.to_s.strip unless blank?(explicit)
|