@zalom/plastic 1.14.0 → 2.0.0-alpha.1
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 +101 -294
- package/README.md +5 -6
- package/agents/plastic-advisor.md +18 -17
- package/agents/plastic-enforcer.md +57 -69
- package/agents/plastic-executor.md +34 -20
- package/agents/plastic-faux-advisor.md +18 -18
- package/config_asks.yml +38 -0
- package/hooks/capture +4 -0
- package/hooks/check-update +12 -3
- package/hooks/close +5 -0
- package/hooks/hooks.json +9 -49
- package/hooks/record +4 -0
- package/package.json +3 -2
- package/scripts/agent-report +8 -9
- package/scripts/append-ledger +195 -0
- package/scripts/codex-hook +101 -95
- package/scripts/dashboard.rb +4 -4
- package/scripts/doctor.rb +106 -43
- package/scripts/end-intent +77 -76
- package/scripts/exec-worktree +11 -20
- package/scripts/file-session-intent +61 -0
- package/scripts/hook-capture +242 -0
- package/scripts/hook-close +34 -0
- package/scripts/hook-record +149 -0
- package/scripts/hook-session-start +95 -12
- package/scripts/install.rb +8 -6
- package/scripts/lib/agent_models.rb +1 -7
- package/scripts/lib/arm.rb +283 -0
- package/scripts/lib/backfill_intent.rb +316 -0
- package/scripts/lib/bridge.rb +24 -1597
- package/scripts/lib/doctor_core.rb +17 -88
- package/scripts/lib/doctor_session_ledger.rb +106 -0
- package/scripts/lib/exec_worktree.rb +14 -60
- package/scripts/lib/harness_text.rb +5 -4
- package/scripts/lib/hook_registry.rb +44 -107
- package/scripts/lib/insights.rb +3 -4
- package/scripts/lib/installer_core.rb +19 -30
- package/scripts/lib/lock.rb +6 -26
- package/scripts/lib/outcome_guard.rb +7 -5
- package/scripts/lib/project_validator.rb +30 -1
- package/scripts/lib/roadmap_queue.rb +3 -3
- package/scripts/lib/roadmap_savepoint.rb +1 -1
- package/scripts/lib/rule_catalog.rb +6 -3
- package/scripts/lib/savepoint.rb +320 -0
- package/scripts/lib/scaffold_intent.rb +13 -179
- package/scripts/lib/session_backfill.rb +184 -0
- package/scripts/lib/session_close.rb +84 -0
- package/scripts/lib/session_git.rb +469 -0
- package/scripts/lib/session_ledger.rb +497 -0
- package/scripts/lib/store_discovery.rb +1 -1
- package/scripts/lib/store_provisioning.rb +1 -1
- package/scripts/maintenance-run +7 -7
- package/scripts/new-intent +63 -4
- package/scripts/plastic-lock +94 -41
- package/scripts/promote-session-item +150 -0
- package/scripts/rebuild-graph +1 -1
- package/scripts/roadmap-next +2 -2
- package/scripts/rollback.rb +42 -3
- package/scripts/scaffold-intent +37 -63
- package/scripts/session-commit +116 -0
- package/scripts/spawn-preamble +5 -5
- package/scripts/update.rb +7 -3
- package/skills/agent-advisor/SKILL.md +3 -3
- package/skills/agent-advisor/references/advisor-protocol.md +20 -20
- package/skills/auto/SKILL.md +218 -317
- package/skills/auto/evals/evals.json +10 -10
- package/skills/auto/references/agent-architecture.md +102 -90
- package/skills/auto/references/agent-report-contract.md +4 -21
- package/skills/auto/references/end-tail.md +47 -53
- package/skills/auto/references/human-report-contract.md +8 -8
- package/skills/conventions/SKILL.md +1 -3
- package/skills/conventions/references/lifecycle-and-savepoints.md +35 -1
- package/skills/conventions/references/locks-and-worktrees.md +47 -55
- package/skills/conventions/references/maintenance-and-revisions.md +12 -12
- package/skills/dashboard/SKILL.md +1 -1
- package/skills/direct/SKILL.md +66 -0
- package/skills/direct/references/request-signals.md +59 -0
- package/skills/doctor/SKILL.md +58 -11
- package/skills/doctor/report.md +3 -1
- package/skills/install/SKILL.md +11 -9
- package/skills/intent-continuing/SKILL.md +131 -89
- package/skills/intent-continuing/references/boarding-matrix.md +34 -0
- package/skills/intent-continuing/references/context-management.md +2 -2
- package/skills/intent-creating/SKILL.md +2 -2
- package/skills/intent-creating/references/lifecycle.md +1 -1
- package/skills/intent-ending/SKILL.md +28 -37
- package/skills/intent-ending/evals/evals.json +4 -4
- package/skills/intent-executing/SKILL.md +12 -49
- package/skills/intent-executing/plan-reviewer-prompt.md +39 -0
- package/skills/intent-speccing/SKILL.md +108 -51
- package/skills/intent-speccing/evals/evals.json +5 -5
- package/skills/intent-speccing/references/per-section-fill-rules.md +4 -4
- package/skills/intent-speccing/references/self-verify-checklist.md +1 -2
- package/skills/project-creating/SKILL.md +1 -1
- package/skills/releasing/SKILL.md +7 -8
- package/skills/tutorial/SKILL.md +2 -2
- package/skills/tutorial/references/track-1-guided.md +16 -18
- package/skills/tutorial/references/track-2-auto.md +12 -13
- package/skills/tutorial/references/track-3-projects-and-roadmaps.md +2 -2
- package/templates/agents.md +4 -7
- package/templates/project.yml +7 -0
- package/templates/roadmap.md +1 -1
- package/templates/savepoint.md +2 -2
- package/templates/session-intent.md +29 -0
- package/agents/plastic-brainstorming.md +0 -48
- package/agents/plastic-future-intent-researcher.md +0 -35
- package/agents/plastic-intent-curator.md +0 -47
- package/agents/plastic-intent-discovery.md +0 -43
- package/agents/plastic-planner.md +0 -56
- package/agents/plastic-spec-specialist.md +0 -53
- package/hooks/auto-arm +0 -5
- package/hooks/bash-gate +0 -3
- package/hooks/continue +0 -31
- package/hooks/edit-gates +0 -3
- package/hooks/future-intent-check +0 -25
- package/hooks/gate-check +0 -12
- package/hooks/power-tools +0 -8
- package/scripts/hook-auto-arm +0 -51
- package/scripts/hook-bash-gate +0 -78
- package/scripts/hook-code-gate +0 -38
- package/scripts/hook-continue +0 -48
- package/scripts/hook-create-gate +0 -51
- package/scripts/hook-edit-gates +0 -58
- package/scripts/hook-future-intent-check +0 -90
- package/scripts/hook-gate-check +0 -169
- package/scripts/hook-links-gate +0 -54
- package/scripts/hook-lock-gate +0 -47
- package/scripts/hook-power-tools +0 -38
- package/scripts/hook-savepoint-pre +0 -32
- package/scripts/lib/codex_edit_gates.rb +0 -138
- package/scripts/lib/edit_gates.rb +0 -398
- package/scripts/lib/links_gate.rb +0 -140
- package/scripts/lib/qmd_hook.rb +0 -41
- package/scripts/lib/spec_header.rb +0 -83
- package/scripts/lib/start_intent.rb +0 -296
- package/scripts/start-intent +0 -89
- package/skills/_active-intent-gate.md +0 -26
- package/skills/auto/references/tiers.md +0 -100
- package/skills/continuing/SKILL.md +0 -34
- package/skills/continuing/evals/evals.json +0 -91
- package/skills/conventions/references/gates-and-enforcement.md +0 -53
- package/skills/conventions/references/tiers-and-dispatch.md +0 -135
- package/skills/doctor/references/gates-stuck-detection.md +0 -43
- package/skills/intent-brainstorming/SKILL.md +0 -118
- package/skills/intent-brainstorming/evals/evals.json +0 -67
- package/skills/intent-continuing/evals/evals.json +0 -145
- package/skills/intent-discovering/SKILL.md +0 -56
- package/skills/intent-grilling/SKILL.md +0 -108
- package/skills/intent-linking/SKILL.md +0 -128
- package/skills/intent-linking/evals/evals.json +0 -22
- package/skills/intent-linking/references/zettelkasten.md +0 -45
- package/skills/intent-locking/SKILL.md +0 -64
- package/skills/intent-planning/SKILL.md +0 -145
- package/skills/intent-planning/evals/evals.json +0 -81
- package/skills/intent-planning/references/plan-format.md +0 -130
- package/skills/intent-researching/SKILL.md +0 -123
- package/skills/intent-researching/evals/evals.json +0 -22
- package/skills/intent-savepoint/SKILL.md +0 -83
- package/skills/intent-savepoint/references/context-management.md +0 -32
- package/skills/intent-starting/SKILL.md +0 -151
- package/skills/intent-starting/evals/evals.json +0 -117
- package/skills/intent-starting/references/boarding-matrix.md +0 -35
- package/skills/project-continuing/SKILL.md +0 -119
- package/skills/project-continuing/evals/evals.json +0 -100
- package/skills/roadmap-continuing/SKILL.md +0 -89
- package/skills/roadmap-continuing/evals/evals.json +0 -82
- package/skills/skill-creating/SKILL.md +0 -75
- package/skills/skill-creating/evals/evals.json +0 -108
- package/skills/skill-creating/references/agents.md +0 -168
- package/skills/skill-creating/references/defaults-first.md +0 -23
- package/skills/skill-creating/references/evals.md +0 -41
- package/skills/skill-creating/references/hooks.md +0 -251
- package/skills/skill-creating/references/progressive-disclosure.md +0 -176
- package/skills/skill-creating/references/scripts.md +0 -166
- package/skills/skill-creating/references/skills.md +0 -169
- package/skills/skill-creating/scripts/scaffold.rb +0 -313
- package/skills/skill-evaluating/SKILL.md +0 -141
- package/skills/skill-evaluating/assets/eval-template.json +0 -12
- package/skills/skill-evaluating/evals/evals.json +0 -75
- package/skills/skill-evaluating/references/convention-checks.md +0 -76
- package/skills/skill-evaluating/references/eval-methodology.md +0 -154
- package/skills/store-curating/SKILL.md +0 -64
- package/skills/store-curating/evals/evals.json +0 -38
- package/skills/store-indexing/SKILL.md +0 -93
- package/skills/store-indexing/evals/evals.json +0 -22
- package/skills/store-indexing/references/zettelkasten-linking.md +0 -32
- package/skills/store-provisioning/SKILL.md +0 -55
- /package/skills/{project-continuing → intent-continuing}/references/board-fill.md +0 -0
- /package/skills/{roadmap-continuing → intent-continuing}/references/liveness-ranking.md +0 -0
- /package/skills/{intent-brainstorming → intent-speccing}/references/design-principles.md +0 -0
package/scripts/doctor.rb
CHANGED
|
@@ -21,7 +21,7 @@ require_relative "lib/store_discovery"
|
|
|
21
21
|
require_relative "lib/links_projection"
|
|
22
22
|
require_relative "lib/links_section"
|
|
23
23
|
require_relative "lib/lock"
|
|
24
|
-
require_relative "lib/
|
|
24
|
+
require_relative "lib/savepoint"
|
|
25
25
|
require_relative "lib/agent_models"
|
|
26
26
|
require_relative "lib/outcome_guard"
|
|
27
27
|
require_relative "lib/skill_lint"
|
|
@@ -29,6 +29,7 @@ require_relative "lib/config_asks"
|
|
|
29
29
|
require_relative "lib/power_tools"
|
|
30
30
|
require_relative "lib/preflight"
|
|
31
31
|
require_relative "lib/ruby_probe"
|
|
32
|
+
require_relative "lib/doctor_session_ledger"
|
|
32
33
|
|
|
33
34
|
# Diagnostic engine, instantiable with an injected store/agent map so tests can
|
|
34
35
|
# run it hermetically (no eval, no global-constant rewriting).
|
|
@@ -37,6 +38,7 @@ require_relative "lib/ruby_probe"
|
|
|
37
38
|
# store, conventions, intent and CLI halves, so Doctor stays one class with one
|
|
38
39
|
# public surface.
|
|
39
40
|
class Doctor
|
|
41
|
+
include DoctorSessionLedger
|
|
40
42
|
|
|
41
43
|
REQUIRED_INDEX_SECTIONS = ["## Active", "## Future", "## Clusters", "## Abandoned", "## Completed"].freeze
|
|
42
44
|
|
|
@@ -431,7 +433,7 @@ class Doctor
|
|
|
431
433
|
message: "#{bad_sections.size} intent file(s) have non-sanctioned ## sections",
|
|
432
434
|
details: bad_sections.map { |b| "#{b[:dir]}: #{b[:issues].join(", ")}" },
|
|
433
435
|
fixable: true,
|
|
434
|
-
fix_hint: "
|
|
436
|
+
fix_hint: "Relocate each unsanctioned section into the " \
|
|
435
437
|
"intent's revisions.md via move-and-record (a missing required section is restored " \
|
|
436
438
|
"or reprojected instead); see plastic-conventions > references/maintenance-and-revisions.md"
|
|
437
439
|
)
|
|
@@ -553,9 +555,9 @@ class Doctor
|
|
|
553
555
|
# suppress) is exactly what makes the row dead.
|
|
554
556
|
def done_signal_findings_for_dir(dir, label:, scope:, dirname:, terminal:, active:, excluded_rules: [])
|
|
555
557
|
outcome = File.join(dir, "outcome.md")
|
|
556
|
-
outcome_real =
|
|
558
|
+
outcome_real = Savepoint.stage_file_present?(outcome)
|
|
557
559
|
findings = { conflict: nil, phantom: nil, gap: [], operational_gap: [], excluded: [],
|
|
558
|
-
excluded_rules_fired: [], stalled: nil }
|
|
560
|
+
excluded_rules_fired: [], stalled: nil, unbackfilled: [], excluded_backfill: [] }
|
|
559
561
|
|
|
560
562
|
# HARD conflict: the deliverable exists but INDEX still says Active. This
|
|
561
563
|
# is the one true INDEX-wins disagreement, so it stays a fail.
|
|
@@ -569,7 +571,7 @@ def done_signal_findings_for_dir(dir, label:, scope:, dirname:, terminal:, activ
|
|
|
569
571
|
# the generic replacement for the frozen 170a amnesty list is simply "never suppress" -
|
|
570
572
|
# the message already labels a terminal phantom report-only, so dropping the id-based
|
|
571
573
|
# suppression is strictly more general and needs no per-store state at all).
|
|
572
|
-
phantom_lines =
|
|
574
|
+
phantom_lines = Savepoint.savepoint_phantom_lines(dir)
|
|
573
575
|
if phantom_lines.any?
|
|
574
576
|
detail = phantom_lines.map { |line, reason| "#{line} (#{reason})" }.join("; ")
|
|
575
577
|
scope_note = terminal ? "terminal in INDEX, report-only (immutable history)" : "live intent, auto-rebuildable"
|
|
@@ -583,7 +585,7 @@ def done_signal_findings_for_dir(dir, label:, scope:, dirname:, terminal:, activ
|
|
|
583
585
|
if terminal && !outcome_real
|
|
584
586
|
state = File.exist?(outcome) ? "still a placeholder" : "missing"
|
|
585
587
|
findings[:gap] << "#{label}: terminal in INDEX but outcome.md is #{state} " \
|
|
586
|
-
"(delivery claim -
|
|
588
|
+
"(delivery claim; written at close by end-intent since intent 308)"
|
|
587
589
|
end
|
|
588
590
|
|
|
589
591
|
if terminal
|
|
@@ -603,6 +605,21 @@ def done_signal_findings_for_dir(dir, label:, scope:, dirname:, terminal:, activ
|
|
|
603
605
|
end
|
|
604
606
|
findings[:excluded_rules_fired] << "savepoint_operational" if suppressed && findings[:excluded].any?
|
|
605
607
|
|
|
608
|
+
# Unbackfilled documents (intent 308): spec.md or plan.md missing or still the
|
|
609
|
+
# placeholder, or no real action file, on a terminal intent. Every one of these is
|
|
610
|
+
# written from the record by `scaffold-intent backfill`, so this is repairable and
|
|
611
|
+
# reported as a fixable warn (backfilled_complete). Its exclusions go to their OWN
|
|
612
|
+
# bucket so savepoint_operational's consumed/dead-row bookkeeping above never sees them.
|
|
613
|
+
backfill_gaps = %w[spec.md plan.md].reject { |f| Savepoint.stage_file_present?(File.join(dir, f)) }
|
|
614
|
+
backfill_gaps << "actions/" unless Savepoint.has_real_action?(dir)
|
|
615
|
+
if backfill_gaps.any?
|
|
616
|
+
suppressed_backfill = excluded_rules.include?("backfilled_complete")
|
|
617
|
+
target = suppressed_backfill ? findings[:excluded_backfill] : findings[:unbackfilled]
|
|
618
|
+
target << "#{label}: terminal in INDEX but #{backfill_gaps.join(", ")} " \
|
|
619
|
+
"#{backfill_gaps.size == 1 ? "is" : "are"} missing or still a placeholder (backfillable)"
|
|
620
|
+
findings[:excluded_rules_fired] << "backfilled_complete" if suppressed_backfill
|
|
621
|
+
end
|
|
622
|
+
|
|
606
623
|
# Stalled completion: unchanged, never consulted amnesty.
|
|
607
624
|
if File.exist?(Lock.path(dir))
|
|
608
625
|
note = Lock.fresh?(dir) ? "delivery.lock still present (post-done window not closed)"
|
|
@@ -626,6 +643,9 @@ def check_done_signals(scopes: nil)
|
|
|
626
643
|
dead_row_paths = []
|
|
627
644
|
stalled = []
|
|
628
645
|
phantoms = []
|
|
646
|
+
unbackfilled = [] # spec/plan/actions gaps on terminal intents (intent 308) - repairable
|
|
647
|
+
excluded_backfill = [] # backfill gaps knowingly exempted via doctor-exclusions (intent 308)
|
|
648
|
+
dead_rows_by_rule = Hash.new { |h, k| h[k] = [] }
|
|
629
649
|
|
|
630
650
|
done_signal_stores(scopes).each do |store|
|
|
631
651
|
exclusions = DoctorExclusions.load(store[:index])
|
|
@@ -634,7 +654,7 @@ def check_done_signals(scopes: nil)
|
|
|
634
654
|
exclusion_error_paths << exclusions[:path]
|
|
635
655
|
end
|
|
636
656
|
|
|
637
|
-
consumed = { "savepoint_operational" => [] }
|
|
657
|
+
consumed = { "savepoint_operational" => [], "backfilled_complete" => [] }
|
|
638
658
|
# `known_ids` (post-review fix): every intent id with a REAL DIRECTORY in this store, scanned
|
|
639
659
|
# directly from disk - independent of INDEX.md. An id can have a directory on disk without
|
|
640
660
|
# being listed in INDEX (a de-indexed "ghost"), and the walk below alone would never visit it;
|
|
@@ -672,6 +692,11 @@ def check_done_signals(scopes: nil)
|
|
|
672
692
|
phantoms << findings[:phantom] if findings[:phantom]
|
|
673
693
|
gaps.concat(findings[:gap])
|
|
674
694
|
operational_gaps.concat(findings[:operational_gap])
|
|
695
|
+
unbackfilled.concat(findings[:unbackfilled])
|
|
696
|
+
if findings[:excluded_backfill].any?
|
|
697
|
+
excluded_backfill.concat(findings[:excluded_backfill])
|
|
698
|
+
exclusion_paths << exclusions[:path]
|
|
699
|
+
end
|
|
675
700
|
if findings[:excluded].any?
|
|
676
701
|
excluded.concat(findings[:excluded])
|
|
677
702
|
exclusion_paths << exclusions[:path]
|
|
@@ -692,7 +717,9 @@ def check_done_signals(scopes: nil)
|
|
|
692
717
|
else
|
|
693
718
|
"names an intent with no current #{row[:rule]} finding"
|
|
694
719
|
end
|
|
695
|
-
|
|
720
|
+
dead_row = "#{store[:scope]}: #{exclusions[:path]}: #{row[:rule]} #{row[:id]} - #{reason}"
|
|
721
|
+
dead_rows << dead_row
|
|
722
|
+
dead_rows_by_rule[row[:rule]] << dead_row
|
|
696
723
|
dead_row_paths << exclusions[:path]
|
|
697
724
|
end
|
|
698
725
|
end
|
|
@@ -733,7 +760,7 @@ def check_done_signals(scopes: nil)
|
|
|
733
760
|
checks << check(
|
|
734
761
|
category: "done_signals", name: "signals_complete", status: "pass",
|
|
735
762
|
message: "#{gaps.size} terminal intent#{gaps.size == 1 ? "" : "s"} missing a real outcome.md " \
|
|
736
|
-
"(delivery claim -
|
|
763
|
+
"(delivery claim; end-intent writes outcome.md at close since intent 308, so this is pre-308 history or a close outside end-intent; informational only, does not affect doctor's exit code)",
|
|
737
764
|
details: gaps
|
|
738
765
|
)
|
|
739
766
|
end
|
|
@@ -750,10 +777,19 @@ def check_done_signals(scopes: nil)
|
|
|
750
777
|
# suppressed nothing this run. It is purely informational, exactly like `exclusion_suffix` - it
|
|
751
778
|
# never changes status on any of the three branches below, because a stale governance-record row
|
|
752
779
|
# is bookkeeping drift, not a store regression (219 D6 is untouched: no disposition is invented).
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
780
|
+
# Per rule (intent 308): savepoint_operational and backfilled_complete each fold in only
|
|
781
|
+
# their own exclusions and dead rows, so one check never carries the other's counts.
|
|
782
|
+
suffixes = lambda do |excluded_rows, dead|
|
|
783
|
+
ex = excluded_rows.empty? ? "" : " (#{excluded_rows.size} excluded via #{exclusion_paths.join(", ")})"
|
|
784
|
+
dd = dead.empty? ? "" : " (#{dead.size} dead row#{dead.size == 1 ? "" : "s"} " \
|
|
785
|
+
"in #{dead_row_paths.join(", ")}, suppressing nothing - prune with " \
|
|
786
|
+
"`maintenance-run --tool register-exclusions --prune`)"
|
|
787
|
+
[ex, dd]
|
|
788
|
+
end
|
|
789
|
+
savepoint_dead = dead_rows_by_rule["savepoint_operational"]
|
|
790
|
+
backfill_dead = dead_rows_by_rule["backfilled_complete"]
|
|
791
|
+
exclusion_suffix, dead_suffix = suffixes.call(excluded, savepoint_dead)
|
|
792
|
+
backfill_exclusion_suffix, backfill_dead_suffix = suffixes.call(excluded_backfill, backfill_dead)
|
|
757
793
|
|
|
758
794
|
if exclusion_errors.any?
|
|
759
795
|
checks << check(
|
|
@@ -762,7 +798,7 @@ def check_done_signals(scopes: nil)
|
|
|
762
798
|
"missing an operational savepoint.md or its Done echo, and " \
|
|
763
799
|
"#{exclusion_errors.size} doctor-exclusions error#{exclusion_errors.size == 1 ? "" : "s"} " \
|
|
764
800
|
"(a malformed exclusion file never suppresses a finding)#{exclusion_suffix}#{dead_suffix}",
|
|
765
|
-
details: operational_gaps + exclusion_errors +
|
|
801
|
+
details: operational_gaps + exclusion_errors + savepoint_dead, fixable: true,
|
|
766
802
|
fix_hint: "Fix the malformed doctor-exclusions file(s) (#{exclusion_error_paths.join(", ")}) - " \
|
|
767
803
|
"format `rule_name id id id`, blank lines and # comments ignored - then reconstruct " \
|
|
768
804
|
"any remaining real gap via `maintenance-run --tool rebuild-savepoint --intent <id> " \
|
|
@@ -774,7 +810,7 @@ def check_done_signals(scopes: nil)
|
|
|
774
810
|
category: "done_signals", name: "savepoint_operational", status: "pass",
|
|
775
811
|
message: "No terminal intent is missing an operational savepoint.md or its Done echo" \
|
|
776
812
|
"#{exclusion_suffix}#{dead_suffix}",
|
|
777
|
-
details:
|
|
813
|
+
details: savepoint_dead
|
|
778
814
|
)
|
|
779
815
|
else
|
|
780
816
|
checks << check(
|
|
@@ -782,13 +818,50 @@ def check_done_signals(scopes: nil)
|
|
|
782
818
|
message: "#{operational_gaps.size} terminal intent#{operational_gaps.size == 1 ? "" : "s"} " \
|
|
783
819
|
"missing an operational savepoint.md or its Done echo (reconstructible)" \
|
|
784
820
|
"#{exclusion_suffix}#{dead_suffix}",
|
|
785
|
-
details: operational_gaps +
|
|
821
|
+
details: operational_gaps + savepoint_dead, fixable: true,
|
|
786
822
|
fix_hint: "Reconstruct the minimal two-line started/Done echo via " \
|
|
787
823
|
"`maintenance-run --tool rebuild-savepoint --intent <id> --apply` (197-conformant: " \
|
|
788
824
|
"receipt-before-write via RevisionsWriter, one intent per invocation, owner-approval-gated)."
|
|
789
825
|
)
|
|
790
826
|
end
|
|
791
827
|
|
|
828
|
+
# backfilled_complete (intent 308): spec.md, plan.md, or a real action file missing on a
|
|
829
|
+
# terminal intent. Same three branches as savepoint_operational: a malformed exclusion
|
|
830
|
+
# file is always loud, a clean set passes with its own exclusion and dead-row counts
|
|
831
|
+
# folded in, a real gap set warns with the backfill verb as the fix.
|
|
832
|
+
backfill_hint = "Write the missing documents from the record via " \
|
|
833
|
+
"`scaffold-intent backfill --store <store> --id <id> --disposition " \
|
|
834
|
+
"<delivered|abandoned>` (never touches real content, one intent per invocation)."
|
|
835
|
+
if exclusion_errors.any?
|
|
836
|
+
checks << check(
|
|
837
|
+
category: "done_signals", name: "backfilled_complete", status: "warn",
|
|
838
|
+
message: "#{unbackfilled.size} terminal intent#{unbackfilled.size == 1 ? "" : "s"} " \
|
|
839
|
+
"missing a backfillable document, and #{exclusion_errors.size} doctor-exclusions " \
|
|
840
|
+
"error#{exclusion_errors.size == 1 ? "" : "s"} (a malformed exclusion file never " \
|
|
841
|
+
"suppresses a finding)#{backfill_exclusion_suffix}#{backfill_dead_suffix}",
|
|
842
|
+
details: unbackfilled + exclusion_errors + backfill_dead, fixable: true,
|
|
843
|
+
fix_hint: "Fix the malformed doctor-exclusions file(s) (#{exclusion_error_paths.join(", ")}), " \
|
|
844
|
+
"then write the missing documents from the record via `scaffold-intent backfill " \
|
|
845
|
+
"--store <store> --id <id> --disposition <delivered|abandoned>`."
|
|
846
|
+
)
|
|
847
|
+
elsif unbackfilled.empty?
|
|
848
|
+
checks << check(
|
|
849
|
+
category: "done_signals", name: "backfilled_complete", status: "pass",
|
|
850
|
+
message: "Every terminal intent carries a real spec.md, plan.md, and action file" \
|
|
851
|
+
"#{backfill_exclusion_suffix}#{backfill_dead_suffix}",
|
|
852
|
+
details: backfill_dead
|
|
853
|
+
)
|
|
854
|
+
else
|
|
855
|
+
checks << check(
|
|
856
|
+
category: "done_signals", name: "backfilled_complete", status: "warn",
|
|
857
|
+
message: "#{unbackfilled.size} terminal intent#{unbackfilled.size == 1 ? "" : "s"} " \
|
|
858
|
+
"missing a backfillable document (spec.md, plan.md, or a real action file)" \
|
|
859
|
+
"#{backfill_exclusion_suffix}#{backfill_dead_suffix}",
|
|
860
|
+
details: unbackfilled + backfill_dead, fixable: true,
|
|
861
|
+
fix_hint: backfill_hint
|
|
862
|
+
)
|
|
863
|
+
end
|
|
864
|
+
|
|
792
865
|
if stalled.empty?
|
|
793
866
|
checks << check(
|
|
794
867
|
category: "done_signals", name: "stalled_completion", status: "pass",
|
|
@@ -818,8 +891,8 @@ def check_done_signals(scopes: nil)
|
|
|
818
891
|
message: "#{phantoms.size} intent#{phantoms.size == 1 ? "" : "s"} carry savepoint.md " \
|
|
819
892
|
"line(s) contradicted by disk (advisory; terminal history stays report-only)",
|
|
820
893
|
details: phantoms, fixable: true,
|
|
821
|
-
fix_hint: "For a live (Active) intent,
|
|
822
|
-
"
|
|
894
|
+
fix_hint: "For a live (Active) intent, rebuild the ledger via " \
|
|
895
|
+
"Savepoint.rebuild_savepoint. Terminal (Completed/Abandoned) intents are immutable: " \
|
|
823
896
|
"a phantom there stays advisory unless an explicit human grant authorizes the " \
|
|
824
897
|
"124a manual Done-bookend repair."
|
|
825
898
|
)
|
|
@@ -991,9 +1064,9 @@ end
|
|
|
991
1064
|
INTENT_END_LIFECYCLE_FILES = %w[spec.md plan.md checklist.md outcome.md].freeze
|
|
992
1065
|
|
|
993
1066
|
def intent_lifecycle_artifacts_check(intent_dir, disposition)
|
|
994
|
-
missing = INTENT_END_LIFECYCLE_FILES.select { |f| !
|
|
995
|
-
unless
|
|
996
|
-
missing = [File.basename(
|
|
1067
|
+
missing = INTENT_END_LIFECYCLE_FILES.select { |f| !Savepoint.stage_file_present?(File.join(intent_dir, f)) }
|
|
1068
|
+
unless Savepoint.stage_file_present?(Savepoint.intent_file(intent_dir))
|
|
1069
|
+
missing = [File.basename(Savepoint.intent_file(intent_dir))] + missing
|
|
997
1070
|
end
|
|
998
1071
|
|
|
999
1072
|
outcome_note = nil
|
|
@@ -1014,7 +1087,7 @@ end
|
|
|
1014
1087
|
|
|
1015
1088
|
def intent_checklist_complete_check(intent_dir)
|
|
1016
1089
|
path = File.join(intent_dir, "checklist.md")
|
|
1017
|
-
unless
|
|
1090
|
+
unless Savepoint.stage_file_present?(path)
|
|
1018
1091
|
return check(category: "intent_end", name: "intent_checklist_complete", status: "pass",
|
|
1019
1092
|
message: "n/a: checklist.md absent or placeholder (flagged above if that is a gap)")
|
|
1020
1093
|
end
|
|
@@ -1074,7 +1147,8 @@ end
|
|
|
1074
1147
|
# Terminal-gated (281 D3): done_signal_findings_for_dir only ever produces this finding
|
|
1075
1148
|
# inside `if terminal`, so honoring the exclusion for a still-Active intent would suppress a
|
|
1076
1149
|
# 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
|
|
1150
|
+
# the live, repairable warning scripts/end-intent's structure self-check exists to raise
|
|
1151
|
+
# (a report, never a refusal, since intent 308).
|
|
1078
1152
|
#
|
|
1079
1153
|
# Never raises: DoctorExclusions is fail-open by contract (274 D5) and index_sections_by_dir
|
|
1080
1154
|
# returns an empty map for a missing INDEX.
|
|
@@ -1131,9 +1205,9 @@ end
|
|
|
1131
1205
|
first_line = File.read(savepoint).each_line.find { |l| !l.strip.empty? }.to_s.strip
|
|
1132
1206
|
parts = first_line.split(/\s{2,}/)
|
|
1133
1207
|
born_pair = parts.length >= 3 ? [parts[1], parts[2]] : nil
|
|
1134
|
-
expected_pair =
|
|
1208
|
+
expected_pair = Savepoint.savepoint_milestone(intent_dir, File.basename(Savepoint.intent_file(intent_dir)))
|
|
1135
1209
|
|
|
1136
|
-
phantoms =
|
|
1210
|
+
phantoms = Savepoint.savepoint_phantom_lines(intent_dir)
|
|
1137
1211
|
problems = []
|
|
1138
1212
|
problems << "born line #{born_pair.inspect} does not match the expected #{expected_pair.inspect}" \
|
|
1139
1213
|
if born_pair != expected_pair
|
|
@@ -1147,7 +1221,7 @@ end
|
|
|
1147
1221
|
check(category: "intent_end", name: "intent_savepoint_truthful", status: "warn",
|
|
1148
1222
|
message: "#{problems.size} savepoint truthfulness issue(s) (advisory, never blocking)",
|
|
1149
1223
|
details: problems,
|
|
1150
|
-
fixable: true, fix_hint: "
|
|
1224
|
+
fixable: true, fix_hint: "Rebuild the ledger via Savepoint.rebuild_savepoint")
|
|
1151
1225
|
end
|
|
1152
1226
|
end
|
|
1153
1227
|
|
|
@@ -1270,7 +1344,7 @@ end
|
|
|
1270
1344
|
checks << graph_finding_check(
|
|
1271
1345
|
"graph_i4_danglers", i4,
|
|
1272
1346
|
"Every sources/chain id resolves to a real intent (I4)",
|
|
1273
|
-
"
|
|
1347
|
+
"Record the dangling sources/chain edge as a " \
|
|
1274
1348
|
"broken-source/broken-chain move-and-record entry in the intent's revisions.md (see " \
|
|
1275
1349
|
"plastic-conventions > references/maintenance-and-revisions.md), or restore the missing intent"
|
|
1276
1350
|
)
|
|
@@ -1297,18 +1371,6 @@ end
|
|
|
1297
1371
|
|
|
1298
1372
|
|
|
1299
1373
|
|
|
1300
|
-
# claude_hooks_implemented (intent 244, the Claude twin of intent 200's Codex
|
|
1301
|
-
# check): the edit-path gates now live as branches inside one dispatcher,
|
|
1302
|
-
# which is exactly the shape that let links-gate ship registered and dead on
|
|
1303
|
-
# Codex. HookRegistry::GATE_TOOLS is the registry side; scripts/hook-edit-gates
|
|
1304
|
-
# is the implementation side. Checked in BOTH directions, so a registered gate
|
|
1305
|
-
# with no branch (always allows, silently) and a branch nobody registers
|
|
1306
|
-
# (dead code) are both reported.
|
|
1307
|
-
#
|
|
1308
|
-
# Read as plain text, never required: the dispatcher has top-level side effects
|
|
1309
|
-
# (it reads $stdin and exits), the same reason codex_dispatcher_gate_names reads
|
|
1310
|
-
# scripts/codex-hook as text.
|
|
1311
|
-
|
|
1312
1374
|
# Plastic skills install as ~/.claude/skills/plastic-<name>/SKILL.md. Pass if at
|
|
1313
1375
|
# least one such skill is present.
|
|
1314
1376
|
|
|
@@ -1357,7 +1419,7 @@ end
|
|
|
1357
1419
|
# dispatcher is an executable script with real top-level side effects (it reads
|
|
1358
1420
|
# $stdin and may exit), so it can never be required or executed to introspect it,
|
|
1359
1421
|
# only read as plain text, mirroring codex_agent_toml_well_formed? above. Pulls
|
|
1360
|
-
#
|
|
1422
|
+
# hook names from the STATE_HOOKS %w[] literal, plus the `when
|
|
1361
1423
|
# "<name>"` labels of the top-level `case gate` statement (stopping at its
|
|
1362
1424
|
# trailing `else`). Line-shape dependent, not AST-safe, disclosed as such in
|
|
1363
1425
|
# docs; the healthy-install pass test against the REAL dispatcher is what proves
|
|
@@ -1407,7 +1469,7 @@ end
|
|
|
1407
1469
|
# Classification per installed agent basename, checked in this order:
|
|
1408
1470
|
# 1. an `agents.models.<basename>` override IS configured -> sanctioned,
|
|
1409
1471
|
# pass/informational, LISTED regardless of whether frontmatter matches
|
|
1410
|
-
# (that mismatch is the override working, e.g. plastic-
|
|
1472
|
+
# (that mismatch is the override working, e.g. plastic-executor: fable).
|
|
1411
1473
|
# 2. no override AND basename is a TIER_DEFAULTS key -> compare frontmatter
|
|
1412
1474
|
# to that default; match is pass (clean), mismatch is real drift (warn).
|
|
1413
1475
|
# 3. no override AND basename is a CONSULTATION_AGENTS member -> not a
|
|
@@ -2178,6 +2240,7 @@ end
|
|
|
2178
2240
|
all_checks += check_qmd
|
|
2179
2241
|
all_checks += check_ruby_runtime
|
|
2180
2242
|
all_checks += check_done_signals(scopes: ["global"])
|
|
2243
|
+
all_checks += check_session_ledger(scopes: ["global"])
|
|
2181
2244
|
all_checks += check_skill_lint
|
|
2182
2245
|
all_checks += check_install_integrity
|
|
2183
2246
|
|
|
@@ -2228,10 +2291,10 @@ end
|
|
|
2228
2291
|
case store
|
|
2229
2292
|
when :all
|
|
2230
2293
|
check_global_store + check_project_stores + check_conventions + check_done_signals +
|
|
2231
|
-
check_qmd(detector: qmd_detector, runner: qmd_runner)
|
|
2294
|
+
check_session_ledger + check_qmd(detector: qmd_detector, runner: qmd_runner)
|
|
2232
2295
|
when :global
|
|
2233
2296
|
check_global_store + check_conventions(scopes: ["global"]) +
|
|
2234
|
-
check_done_signals(scopes: ["global"]) +
|
|
2297
|
+
check_done_signals(scopes: ["global"]) + check_session_ledger(scopes: ["global"]) +
|
|
2235
2298
|
check_qmd(detector: qmd_detector, runner: qmd_runner, collection: "plastic-global")
|
|
2236
2299
|
else
|
|
2237
2300
|
all_checks_for_project_slug(store, qmd_detector: qmd_detector, qmd_runner: qmd_runner,
|
package/scripts/end-intent
CHANGED
|
@@ -28,8 +28,12 @@
|
|
|
28
28
|
# file that exists while no session identity resolves from ANY source
|
|
29
29
|
# also refuses (exit 4) rather than ever reaching Lock.takeover with a
|
|
30
30
|
# blank session, which would delete the lock file and then crash.
|
|
31
|
-
# 1.
|
|
32
|
-
# the
|
|
31
|
+
# 1. Backfill (intent 308): write spec.md, plan.md, actions/ACTION_1.md, and
|
|
32
|
+
# outcome.md from the record wherever a file is missing or still the
|
|
33
|
+
# placeholder (BackfillIntent; real content is never touched). Then run
|
|
34
|
+
# doctor's per-intent structure check and OutcomeGuard as a self-check
|
|
35
|
+
# that reports on stderr and proceeds. Then stamp the intent file's
|
|
36
|
+
# `## Outcome` section with --outcome-summary if given.
|
|
33
37
|
# 2. Move the intent's INDEX.md line from `## Active` into `## Completed`
|
|
34
38
|
# (delivered) or `## Abandoned` (abandoned), dated today, appending
|
|
35
39
|
# --index-note (if given). Accepts a real em dash OR a plain hyphen as
|
|
@@ -37,23 +41,21 @@
|
|
|
37
41
|
# always EMITS the real em dash on write. An id that resolves to neither
|
|
38
42
|
# `## Active` nor the terminal section is a loud failure (exit 1); an id
|
|
39
43
|
# already correctly in the terminal section is a quiet, idempotent success.
|
|
40
|
-
# 3. Append the savepoint `Done` bookend (
|
|
44
|
+
# 3. Append the savepoint `Done` bookend (Savepoint.append_terminal_savepoint).
|
|
41
45
|
# 4. Commit the store repo, unless --no-commit.
|
|
42
|
-
# 5. Disarm (intent 188, D2/D16): check the code worktree (
|
|
43
|
-
#
|
|
44
|
-
# --discard-worktree-changes); call the
|
|
45
|
-
#
|
|
46
|
-
#
|
|
47
|
-
#
|
|
48
|
-
#
|
|
49
|
-
# that a worktree may be orphaned), rather than stranding a committed,
|
|
50
|
-
# terminal intent still holding its lock.
|
|
46
|
+
# 5. Disarm (intent 188, D2/D16; intent 307): check the code worktree (derived
|
|
47
|
+
# from projects.yml and the intent id, Arm.worktree_block) for uncommitted
|
|
48
|
+
# changes first (exit 5 if dirty, unless --discard-worktree-changes); call the
|
|
49
|
+
# disarm seam (default Arm.disarm: worktree released, lock cleared, the session
|
|
50
|
+
# pointer back on the day ledger); verify the durable lock file is actually gone
|
|
51
|
+
# afterward, and release it directly when a foreign-keyed disarm left it (the
|
|
52
|
+
# /tmp bridge this step once read was removed in 2.0, intent 307).
|
|
51
53
|
#
|
|
52
54
|
# Exit codes:
|
|
53
55
|
# 0 ok - the intent is closed and no delivery.lock remains.
|
|
54
56
|
# 1 usage/resolution failure, OR an id that resolves to neither ## Active
|
|
55
57
|
# nor the terminal section (D11). Deliberately double duty; see plan.md.
|
|
56
|
-
# 2 the outcome.md guard
|
|
58
|
+
# 2 retired in 2.0 (intent 308): the outcome.md guard reports and proceeds.
|
|
57
59
|
# 3 steps 1-4 already committed, but the delivery lock genuinely could not
|
|
58
60
|
# be cleared (not a bridge cache miss; see step 5 above).
|
|
59
61
|
# 4 pre-flight refusal: a FRESH foreign lock is held by a live,
|
|
@@ -63,19 +65,19 @@
|
|
|
63
65
|
# (git status itself failed: corrupt index, git missing, not a repo);
|
|
64
66
|
# refused before removal; pass --discard-worktree-changes to override
|
|
65
67
|
# deliberately.
|
|
66
|
-
# 6
|
|
67
|
-
# links out of projection, intent-file malformed, or a lifecycle artifact
|
|
68
|
-
# missing/placeholder); named reasons on stderr; authors nothing
|
|
69
|
-
# (intent 222).
|
|
68
|
+
# 6 retired in 2.0 (intent 308): the structure check reports and proceeds.
|
|
70
69
|
|
|
71
70
|
require "fileutils"
|
|
72
71
|
require "date"
|
|
73
72
|
require "open3"
|
|
74
73
|
require "pathname"
|
|
75
74
|
require_relative "lib/bridge"
|
|
75
|
+
require_relative "lib/arm"
|
|
76
|
+
require_relative "lib/savepoint"
|
|
76
77
|
require_relative "lib/lock"
|
|
77
78
|
require_relative "lib/intent_validator"
|
|
78
79
|
require_relative "lib/outcome_guard"
|
|
80
|
+
require_relative "lib/backfill_intent"
|
|
79
81
|
|
|
80
82
|
DISPOSITIONS = %w[delivered abandoned].freeze
|
|
81
83
|
|
|
@@ -169,44 +171,41 @@ def resolve_plastic_home_and_scope(store)
|
|
|
169
171
|
end
|
|
170
172
|
end
|
|
171
173
|
|
|
172
|
-
#
|
|
173
|
-
#
|
|
174
|
-
#
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
174
|
+
# Backfill (intent 308): write the judgment documents that are missing or still the
|
|
175
|
+
# placeholder from the record (BackfillIntent). Fail-open: a crash here warns and the
|
|
176
|
+
# close proceeds, the same contract as the structure check below.
|
|
177
|
+
def run_backfill(intent_dir, store:, id:, disposition:, summary:)
|
|
178
|
+
result = BackfillIntent.run(intent_dir: intent_dir, store: store, id: id,
|
|
179
|
+
disposition: disposition, summary: summary)
|
|
180
|
+
result[:written].each { |rel| warn "end-intent: backfilled #{rel} from the record" }
|
|
181
|
+
result[:notes].each { |note| warn "end-intent: backfill note: #{note}" }
|
|
182
|
+
rescue StandardError => e
|
|
183
|
+
warn "end-intent: backfill crashed (#{e.message}); proceeding without it"
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Doctor's per-intent structure check (intent 222; a self-check since intent 308). Runs
|
|
187
|
+
# after the backfill and REPORTS: every FAIL or WARN check prints its named reason on
|
|
188
|
+
# stderr and the close proceeds. Nothing here exits; a gap end-intent cannot write itself
|
|
189
|
+
# (an unchecked box, a malformed intent file, links out of projection) is the owner's to
|
|
190
|
+
# fix afterward, and doctor --intent keeps reporting it. A crash is rescued the same way.
|
|
191
|
+
# Injected `gate:` seam: tests drive the real Doctor against a hermetic home, or a raising
|
|
192
|
+
# stub to prove the fail-open contract, with no eval/ENV/global seam.
|
|
181
193
|
#
|
|
182
|
-
# Disposition is deliberately NOT passed to the real gate call
|
|
183
|
-
#
|
|
184
|
-
#
|
|
185
|
-
|
|
186
|
-
# it here would make this gate re-check the SAME disposition match the old guard already
|
|
187
|
-
# owns, needlessly widening which exit-2 fixtures become exit-6. doctor.rb --intent called
|
|
188
|
-
# directly (with --disposition) still exercises the fold-in per D2/acceptance criteria.
|
|
189
|
-
def run_structure_gate(id, store:, gate: ->(gate_home, gate_scope) {
|
|
194
|
+
# Disposition is deliberately NOT passed to the real gate call: OutcomeGuard.reason,
|
|
195
|
+
# called right after this in main, already owns disposition-matching, and
|
|
196
|
+
# intent_lifecycle_artifacts checks outcome.md PRESENCE regardless of disposition.
|
|
197
|
+
def run_structure_check(id, store:, gate: ->(gate_home, gate_scope) {
|
|
190
198
|
require_relative "doctor"
|
|
191
199
|
Doctor.new(plastic_home: gate_home).run_intent_check(id, store: gate_scope)
|
|
192
200
|
})
|
|
193
201
|
gate_home, gate_scope = resolve_plastic_home_and_scope(store)
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
detail = Array(c[:details]).any? ? " (#{Array(c[:details]).join("; ")})" : ""
|
|
199
|
-
warn "end-intent: structure gate refused: #{c[:name]}: #{c[:message]}#{detail}"
|
|
200
|
-
end
|
|
201
|
-
exit 6
|
|
202
|
-
when "warn"
|
|
203
|
-
gate_verdict[:checks].select { |c| c[:status] == "warn" }.each do |c|
|
|
204
|
-
detail = Array(c[:details]).any? ? " (#{Array(c[:details]).join("; ")})" : ""
|
|
205
|
-
warn "end-intent: structure gate warning (proceeding): #{c[:name]}: #{c[:message]}#{detail}"
|
|
206
|
-
end
|
|
202
|
+
verdict = gate.call(gate_home, gate_scope)
|
|
203
|
+
verdict[:checks].select { |c| %w[fail warn].include?(c[:status]) }.each do |c|
|
|
204
|
+
detail = Array(c[:details]).any? ? " (#{Array(c[:details]).join("; ")})" : ""
|
|
205
|
+
warn "end-intent: structure check: #{c[:name]}: #{c[:message]}#{detail}"
|
|
207
206
|
end
|
|
208
207
|
rescue StandardError => e
|
|
209
|
-
warn "end-intent: structure
|
|
208
|
+
warn "end-intent: structure check crashed (#{e.message}); proceeding without it"
|
|
210
209
|
end
|
|
211
210
|
|
|
212
211
|
# outcome.md guard: see OutcomeGuard.reason (scripts/lib/outcome_guard.rb, extracted intent 222)
|
|
@@ -459,21 +458,21 @@ end
|
|
|
459
458
|
|
|
460
459
|
# --- step 5: disarm (D2/D16, intent 188) ------------------------------------
|
|
461
460
|
#
|
|
462
|
-
# Injected seams (D2):
|
|
463
|
-
# dirty-worktree check can resolve the code worktree
|
|
464
|
-
# first), disarm defaults to
|
|
461
|
+
# Injected seams (D2, re-pointed by intent 307): worktree_reader defaults to
|
|
462
|
+
# Arm.worktree_block (so the dirty-worktree check can resolve the code worktree
|
|
463
|
+
# path without disarming first), disarm defaults to Arm.disarm. Tests may inject
|
|
465
464
|
# fakes for in-process unit coverage of this function; the subprocess-driven
|
|
466
465
|
# end-to-end tests exercise the real defaults against a hermetic tmp home.
|
|
467
466
|
#
|
|
468
467
|
# Returns :ok, :dirty (exit 5), or :lock_remains (exit 3). A blank session
|
|
469
468
|
# (D3's "no lock at all" fallback case) is a clean no-op: :ok, nothing touched.
|
|
470
469
|
def run_disarm(intent_dir, id, session, discard_worktree_changes:,
|
|
471
|
-
|
|
472
|
-
disarm: ->(sess,
|
|
470
|
+
worktree_reader: ->(dir) { Arm.worktree_block(intent_dir: dir) },
|
|
471
|
+
disarm: ->(sess, dir) { Arm.disarm(intent_dir: dir, session: sess) })
|
|
473
472
|
return :ok if Bridge.blank?(session)
|
|
474
473
|
|
|
475
|
-
|
|
476
|
-
worktree_code =
|
|
474
|
+
block = worktree_reader.call(intent_dir)
|
|
475
|
+
worktree_code = block.is_a?(Hash) ? block["code"] : nil
|
|
477
476
|
|
|
478
477
|
if worktree_code && Dir.exist?(worktree_code)
|
|
479
478
|
begin
|
|
@@ -505,16 +504,15 @@ def run_disarm(intent_dir, id, session, discard_worktree_changes:,
|
|
|
505
504
|
end
|
|
506
505
|
end
|
|
507
506
|
|
|
508
|
-
disarm.call(session,
|
|
507
|
+
disarm.call(session, intent_dir)
|
|
509
508
|
|
|
510
|
-
# The
|
|
511
|
-
#
|
|
512
|
-
#
|
|
513
|
-
#
|
|
514
|
-
#
|
|
515
|
-
#
|
|
516
|
-
#
|
|
517
|
-
# is still ours to clear.
|
|
509
|
+
# The durable lock file is the truth (AGENTS.md). Arm.disarm releases as the
|
|
510
|
+
# lock's recorded owner, so a lock a delegate or a post-takeover owner is
|
|
511
|
+
# closing under a different key can survive it; that would strand a committed,
|
|
512
|
+
# terminal intent still holding its lock, the exact stalled completion this
|
|
513
|
+
# intent forbids. Pre-flight has already established that this session is the
|
|
514
|
+
# owner, a delegate, or the post-takeover owner, so clearing the durable lock
|
|
515
|
+
# directly IS safe, PROVIDED the lock actually present here is still ours to clear.
|
|
518
516
|
#
|
|
519
517
|
# TOCTOU fix (post-review hardening, intent 188): the first version of this
|
|
520
518
|
# fallback read the lock's OWN recorded owner and handed it straight back to
|
|
@@ -556,8 +554,8 @@ def run_disarm(intent_dir, id, session, discard_worktree_changes:,
|
|
|
556
554
|
end
|
|
557
555
|
|
|
558
556
|
unless File.exist?(Lock.path(intent_dir))
|
|
559
|
-
if
|
|
560
|
-
warn "end-intent: no
|
|
557
|
+
if block.nil?
|
|
558
|
+
warn "end-intent: no worktree block could be derived for intent #{id}; " \
|
|
561
559
|
"the delivery lock was cleared directly from disk, but any worktree this intent " \
|
|
562
560
|
"provisioned was NOT removed. Run /plastic-doctor to check for an orphaned worktree."
|
|
563
561
|
end
|
|
@@ -621,16 +619,10 @@ def main(argv)
|
|
|
621
619
|
exit 4
|
|
622
620
|
end
|
|
623
621
|
|
|
624
|
-
run_structure_gate(id, store: store)
|
|
625
|
-
|
|
626
|
-
reason = OutcomeGuard.reason(intent_dir, disposition)
|
|
627
|
-
if reason
|
|
628
|
-
warn "end-intent: #{reason}"
|
|
629
|
-
exit 2
|
|
630
|
-
end
|
|
631
|
-
|
|
632
622
|
if opts[:dry_run]
|
|
633
623
|
puts "end-intent: dry run for #{File.basename(intent_dir)} (#{disposition})"
|
|
624
|
+
targets = BackfillIntent.targets_for(intent_dir)
|
|
625
|
+
puts " would backfill: #{targets.empty? ? "(nothing)" : targets.join(", ")}"
|
|
634
626
|
puts " would stamp ## Outcome: #{opts[:outcome_summary].inspect}" if opts[:outcome_summary]
|
|
635
627
|
puts " would move INDEX.md entry from ## Active to #{index_target_heading(disposition)} (#{index_path})"
|
|
636
628
|
puts " would append index note: #{opts[:index_note].inspect}" if opts[:index_note]
|
|
@@ -653,9 +645,18 @@ def main(argv)
|
|
|
653
645
|
end
|
|
654
646
|
end
|
|
655
647
|
|
|
648
|
+
# 1. Backfill from the record (intent 308), then the structure self-check and the
|
|
649
|
+
# outcome guard, both report-only since 308.
|
|
650
|
+
run_backfill(intent_dir, store: store, id: id, disposition: disposition,
|
|
651
|
+
summary: opts[:outcome_summary])
|
|
652
|
+
run_structure_check(id, store: store)
|
|
653
|
+
|
|
654
|
+
reason = OutcomeGuard.reason(intent_dir, disposition)
|
|
655
|
+
warn "end-intent: outcome.md: #{reason} (proceeding; doctor --intent #{id} reports it)" if reason
|
|
656
|
+
|
|
656
657
|
# 1b. Intent-file `## Outcome` summary stamp (no-op unless given).
|
|
657
658
|
if opts[:outcome_summary]
|
|
658
|
-
intent_file =
|
|
659
|
+
intent_file = Savepoint.intent_file(intent_dir)
|
|
659
660
|
if File.exist?(intent_file)
|
|
660
661
|
original = File.read(intent_file)
|
|
661
662
|
updated = stamp_outcome_summary(original, opts[:outcome_summary])
|
|
@@ -682,7 +683,7 @@ def main(argv)
|
|
|
682
683
|
end
|
|
683
684
|
|
|
684
685
|
# 3. Savepoint Done bookend (idempotent).
|
|
685
|
-
|
|
686
|
+
Savepoint.append_terminal_savepoint(intent_dir, disposition)
|
|
686
687
|
|
|
687
688
|
# 4. Store auto-commit, unless --no-commit. Scoped to this intent's own paths (D17):
|
|
688
689
|
# never git add -A.
|