@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
|
@@ -26,7 +26,9 @@ class Doctor
|
|
|
26
26
|
"hermes" => { name: "Hermes", dir: File.join(Dir.home, ".hermes") },
|
|
27
27
|
}.freeze
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
# The Claude events hooks_registered expects in settings.json: the five-event map of
|
|
30
|
+
# cut-inventory 3b (intent 309 added SessionEnd, registered for close since intent 301).
|
|
31
|
+
CLAUDE_HOOK_EVENTS = %w[SessionStart PreCompact PostToolUse UserPromptSubmit SessionEnd].freeze
|
|
30
32
|
|
|
31
33
|
# Launchers the installer places in the agent's hooks dir that are NOT hooks
|
|
32
34
|
# (intent 204): plastic-statusline is the settings["statusLine"] command, wired
|
|
@@ -40,9 +42,8 @@ class Doctor
|
|
|
40
42
|
folgezettel-id
|
|
41
43
|
read-config
|
|
42
44
|
hook-session-start
|
|
43
|
-
hook-
|
|
44
|
-
hook-
|
|
45
|
-
hook-gate-check
|
|
45
|
+
hook-capture
|
|
46
|
+
hook-record
|
|
46
47
|
validate-intent
|
|
47
48
|
doctor.rb
|
|
48
49
|
].freeze
|
|
@@ -329,7 +330,7 @@ class Doctor
|
|
|
329
330
|
hooks_dir = File.join(agent_dir, "hooks")
|
|
330
331
|
|
|
331
332
|
# Derived from HookRegistry.events (intent 204), not a hand-kept list, so
|
|
332
|
-
# every registered hook
|
|
333
|
+
# every registered hook is checked.
|
|
333
334
|
expected_launchers = HookRegistry.claude_launcher_names
|
|
334
335
|
|
|
335
336
|
# hooks_exist
|
|
@@ -396,12 +397,6 @@ class Doctor
|
|
|
396
397
|
)
|
|
397
398
|
end
|
|
398
399
|
|
|
399
|
-
# claude_hooks_implemented (intent 244): the edit-path gates now live as
|
|
400
|
-
# branches inside one dispatcher (scripts/hook-edit-gates), which is
|
|
401
|
-
# exactly the shape that let links-gate ship registered and dead on
|
|
402
|
-
# Codex (intent 200's failure class, one level up).
|
|
403
|
-
checks << claude_hooks_implemented_check
|
|
404
|
-
|
|
405
400
|
# hooks_registered — settings.json has Plastic hooks for required events
|
|
406
401
|
settings_path = File.join(agent_dir, "settings.json")
|
|
407
402
|
settings = read_json_safe(settings_path)
|
|
@@ -456,7 +451,7 @@ class Doctor
|
|
|
456
451
|
|
|
457
452
|
# hooks_match_registry (intent 108, D7): the live settings must carry
|
|
458
453
|
# EXACTLY the registrations HookRegistry defines; any drift (a missing
|
|
459
|
-
#
|
|
454
|
+
# hook, a stray plastic hook, a stale matcher) is how a hook shipped
|
|
460
455
|
# dead once already.
|
|
461
456
|
expected = HookRegistry.claude_settings_hooks(hook_dir: hooks_dir)
|
|
462
457
|
diffs = []
|
|
@@ -569,71 +564,6 @@ class Doctor
|
|
|
569
564
|
)
|
|
570
565
|
end
|
|
571
566
|
|
|
572
|
-
def claude_dispatcher_gate_names(source)
|
|
573
|
-
case_start = source.index(/^\s*case gate\b/)
|
|
574
|
-
return nil unless case_start
|
|
575
|
-
|
|
576
|
-
case_body = source[case_start..-1]
|
|
577
|
-
end_idx = case_body.index(/^\s*end\b/)
|
|
578
|
-
scanned = end_idx ? case_body[0...end_idx] : case_body
|
|
579
|
-
names = scanned.scan(/^\s*when\s+"([^"]+)"/).flatten.uniq
|
|
580
|
-
names.empty? ? nil : names
|
|
581
|
-
end
|
|
582
|
-
|
|
583
|
-
def claude_hooks_implemented_check
|
|
584
|
-
dispatcher_path = File.join(plastic_home, "scripts", "hook-edit-gates")
|
|
585
|
-
|
|
586
|
-
unless File.exist?(dispatcher_path)
|
|
587
|
-
return check(
|
|
588
|
-
category: "agent_registration", name: "claude_hooks_implemented", status: "fail",
|
|
589
|
-
message: "scripts/hook-edit-gates not found at #{tilde(dispatcher_path)}; cannot verify " \
|
|
590
|
-
"the edit-path gate registry and dispatcher agree",
|
|
591
|
-
fixable: true, fix_hint: "Re-run the Plastic installer"
|
|
592
|
-
)
|
|
593
|
-
end
|
|
594
|
-
|
|
595
|
-
dispatcher_names = claude_dispatcher_gate_names(File.read(dispatcher_path))
|
|
596
|
-
|
|
597
|
-
if dispatcher_names.nil?
|
|
598
|
-
return check(
|
|
599
|
-
category: "agent_registration", name: "claude_hooks_implemented", status: "fail",
|
|
600
|
-
message: "Could not read any gate names out of #{tilde(dispatcher_path)}: the `case gate` " \
|
|
601
|
-
"statement no longer matches the shape this check expects, so the registry was " \
|
|
602
|
-
"never actually checked against the real dispatcher. Update " \
|
|
603
|
-
"claude_dispatcher_gate_names in doctor.rb to the file's new shape.",
|
|
604
|
-
fixable: false
|
|
605
|
-
)
|
|
606
|
-
end
|
|
607
|
-
|
|
608
|
-
registry_names = HookRegistry::GATE_TOOLS.keys
|
|
609
|
-
missing_branch = registry_names - dispatcher_names
|
|
610
|
-
dead_branch = dispatcher_names - registry_names
|
|
611
|
-
|
|
612
|
-
if missing_branch.empty? && dead_branch.empty?
|
|
613
|
-
return check(
|
|
614
|
-
category: "agent_registration", name: "claude_hooks_implemented", status: "pass",
|
|
615
|
-
message: "Every registered edit-path gate has a scripts/hook-edit-gates branch, and every " \
|
|
616
|
-
"dispatcher branch is registered"
|
|
617
|
-
)
|
|
618
|
-
end
|
|
619
|
-
|
|
620
|
-
details = missing_branch.map do |name|
|
|
621
|
-
"#{name} is in HookRegistry::GATE_TOOLS but scripts/hook-edit-gates has no branch for it, " \
|
|
622
|
-
"so the dispatcher skips it on every edit and it never blocks anything"
|
|
623
|
-
end
|
|
624
|
-
details += dead_branch.map do |name|
|
|
625
|
-
"#{name} has a branch in scripts/hook-edit-gates but is not in HookRegistry::GATE_TOOLS, so " \
|
|
626
|
-
"the dispatcher never reaches it and it is dead code"
|
|
627
|
-
end
|
|
628
|
-
|
|
629
|
-
check(
|
|
630
|
-
category: "agent_registration", name: "claude_hooks_implemented", status: "fail",
|
|
631
|
-
message: "The edit-path gate registry and scripts/hook-edit-gates disagree on " \
|
|
632
|
-
"#{details.size} gate(s)",
|
|
633
|
-
details: details, fixable: false
|
|
634
|
-
)
|
|
635
|
-
end
|
|
636
|
-
|
|
637
567
|
def flat_skills_check(agent_dir, installer_flag)
|
|
638
568
|
skills_root = File.join(agent_dir, "skills")
|
|
639
569
|
found = Dir.glob(File.join(skills_root, "plastic-*", "SKILL.md"))
|
|
@@ -907,22 +837,21 @@ class Doctor
|
|
|
907
837
|
# by hand), or a dispatcher branch nobody registers (bash-gate's shape, intent
|
|
908
838
|
# 203, in the opposite direction). This check closes both directions at once.
|
|
909
839
|
|
|
910
|
-
# The Codex
|
|
911
|
-
#
|
|
912
|
-
# (
|
|
913
|
-
#
|
|
914
|
-
# own Ruby constants.
|
|
840
|
+
# The Codex hook names HookRegistry actually registers: the apply_patch record
|
|
841
|
+
# hook (CODEX_POST_HOOKS) and the live-state hook names Codex inherits whole from
|
|
842
|
+
# `events` (CODEX_LIVE_STATE_EVENTS), plus the SessionEnd close hook (intent 309,
|
|
843
|
+
# CODEX_SESSION_END_HOOKS). The PreToolUse gate names left in 2.0 (intent 302). No
|
|
844
|
+
# parsing needed: these are HookRegistry's own Ruby constants.
|
|
915
845
|
def codex_registry_gate_names
|
|
916
846
|
live_state = HookRegistry::CODEX_LIVE_STATE_EVENTS.flat_map do |event|
|
|
917
847
|
HookRegistry.events[event].flat_map { |g| g["hooks"].map { |h| h["name"] } }
|
|
918
848
|
end
|
|
919
|
-
(HookRegistry::
|
|
920
|
-
HookRegistry::CODEX_BASH_HOOKS + live_state).uniq
|
|
849
|
+
(HookRegistry::CODEX_POST_HOOKS + live_state + HookRegistry::CODEX_SESSION_END_HOOKS).uniq
|
|
921
850
|
end
|
|
922
851
|
|
|
923
852
|
def codex_dispatcher_gate_names(source)
|
|
924
853
|
names = []
|
|
925
|
-
%w[STATE_HOOKS
|
|
854
|
+
%w[STATE_HOOKS].each do |const|
|
|
926
855
|
m = source.match(/^#{const}\s*=\s*%w\[([^\]]*)\]/)
|
|
927
856
|
names.concat(m[1].split(/\s+/)) if m
|
|
928
857
|
end
|
|
@@ -960,7 +889,7 @@ class Doctor
|
|
|
960
889
|
return check(
|
|
961
890
|
category: "agent_registration", name: "codex_hooks_implemented", status: "fail",
|
|
962
891
|
message: "Could not read any gate names out of #{tilde(dispatcher_path)}: the " \
|
|
963
|
-
"STATE_HOOKS
|
|
892
|
+
"STATE_HOOKS constant and the `case gate` statement no longer " \
|
|
964
893
|
"match the shape this check expects, so the registry could not be checked " \
|
|
965
894
|
"against the real dispatcher. This is exactly the silent-pass failure this " \
|
|
966
895
|
"check exists to prevent; update codex_dispatcher_gate_names in doctor.rb " \
|
|
@@ -1005,8 +934,8 @@ class Doctor
|
|
|
1005
934
|
toml = File.read(config_toml) rescue ""
|
|
1006
935
|
warns = []
|
|
1007
936
|
# guide Part 3: `codex_hooks` is a deprecated alias for `[features] hooks`; catch both.
|
|
1008
|
-
warns << "hooks are disabled ([features] hooks = false); Plastic
|
|
1009
|
-
warns << "sandbox_mode = \"read-only\"; apply_patch writes (and
|
|
937
|
+
warns << "hooks are disabled ([features] hooks = false); Plastic hooks will not fire" if toml.match?(/^\s*(?:codex_)?hooks\s*=\s*false/)
|
|
938
|
+
warns << "sandbox_mode = \"read-only\"; apply_patch writes (and the record hook) cannot run" if toml.match?(/^\s*sandbox_mode\s*=\s*["']read-only["']/)
|
|
1010
939
|
return nil if warns.empty?
|
|
1011
940
|
|
|
1012
941
|
check(
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "time"
|
|
5
|
+
require_relative "session_ledger"
|
|
6
|
+
|
|
7
|
+
# DoctorSessionLedger (intent 308) - the `session_ledger` doctor category, mixed into
|
|
8
|
+
# Doctor. Two checks over the batch-1 store areas that Doctor's intent walker never
|
|
9
|
+
# visits (both are dot-prefixed, and the walker stays ignorant of them by design: see
|
|
10
|
+
# test/store_walker_compat_test.rb). They live in the GLOBAL store only: every writer of
|
|
11
|
+
# .tmp/ and .sessions/ (hook-capture, hook-record, hook-session-start, Arm, SessionClose)
|
|
12
|
+
# targets the global store, so a project scope gets no finding and no check.
|
|
13
|
+
#
|
|
14
|
+
# Read-only: the orphan check names directories and never removes one, because a
|
|
15
|
+
# heartbeat is only refreshed on prompts and edits, so an old heartbeat is evidence, not
|
|
16
|
+
# proof, that the session is gone. Fail-open: an unreadable directory is skipped, never
|
|
17
|
+
# raised. Depends on the including class for `plastic_home` and `check`.
|
|
18
|
+
module DoctorSessionLedger
|
|
19
|
+
ORPHAN_TTL_SECONDS = 24 * 60 * 60
|
|
20
|
+
|
|
21
|
+
# Seconds since the session's last heartbeat: the ISO-8601 content of `heartbeat`,
|
|
22
|
+
# else that file's mtime, else the directory's mtime.
|
|
23
|
+
def heartbeat_age(dir, now)
|
|
24
|
+
heartbeat = File.join(dir, "heartbeat")
|
|
25
|
+
if File.file?(heartbeat)
|
|
26
|
+
begin
|
|
27
|
+
return now - Time.iso8601(File.read(heartbeat).strip)
|
|
28
|
+
rescue ArgumentError, IOError, SystemCallError
|
|
29
|
+
return now - File.mtime(heartbeat)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
now - File.mtime(dir)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def check_session_ledger(scopes: nil, now: Time.now)
|
|
36
|
+
return [] unless scopes.nil? || scopes.include?("global")
|
|
37
|
+
|
|
38
|
+
store_dir = File.join(plastic_home, "store")
|
|
39
|
+
orphans = orphaned_session_dirs(store_dir, now)
|
|
40
|
+
shape = day_ledger_shape_problems(store_dir)
|
|
41
|
+
|
|
42
|
+
checks = []
|
|
43
|
+
checks << if orphans.empty?
|
|
44
|
+
check(category: "session_ledger", name: "orphaned_session_tmp", status: "pass",
|
|
45
|
+
message: "No .tmp/<session>/ directory has a heartbeat older than " \
|
|
46
|
+
"#{ORPHAN_TTL_SECONDS / 3600} hours")
|
|
47
|
+
else
|
|
48
|
+
check(category: "session_ledger", name: "orphaned_session_tmp", status: "warn",
|
|
49
|
+
message: "#{orphans.size} .tmp/<session>/ director#{orphans.size == 1 ? "y" : "ies"} " \
|
|
50
|
+
"with a heartbeat older than #{ORPHAN_TTL_SECONDS / 3600} hours " \
|
|
51
|
+
"(the session close hook never ran)",
|
|
52
|
+
details: orphans, fixable: true,
|
|
53
|
+
fix_hint: "Remove each listed .tmp/<session>/ directory after confirming that " \
|
|
54
|
+
"session is gone: a live session rewrites its heartbeat on every " \
|
|
55
|
+
"prompt and edit, so only a listed directory may be removed, by hand.")
|
|
56
|
+
end
|
|
57
|
+
checks << if shape.empty?
|
|
58
|
+
check(category: "session_ledger", name: "day_ledger_shape", status: "pass",
|
|
59
|
+
message: "Every .sessions/ entry is a YYYYMMDD day directory with its <day>.md file")
|
|
60
|
+
else
|
|
61
|
+
check(category: "session_ledger", name: "day_ledger_shape", status: "warn",
|
|
62
|
+
message: "#{shape.size} .sessions/ shape problem#{shape.size == 1 ? "" : "s"}",
|
|
63
|
+
details: shape, fixable: true,
|
|
64
|
+
fix_hint: "For a day directory missing its <day>.md, run " \
|
|
65
|
+
"`ruby ~/.plastic/scripts/file-session-intent --day <day> --carry-to " \
|
|
66
|
+
"<today> --store <store>`; rename or remove an entry that is not a " \
|
|
67
|
+
"YYYYMMDD day directory.")
|
|
68
|
+
end
|
|
69
|
+
checks
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def orphaned_session_dirs(store_dir, now)
|
|
73
|
+
tmp_root = SessionLedger.tmp_root(store_dir)
|
|
74
|
+
return [] unless File.directory?(tmp_root)
|
|
75
|
+
|
|
76
|
+
Dir.children(tmp_root).sort.filter_map do |name|
|
|
77
|
+
dir = File.join(tmp_root, name)
|
|
78
|
+
next unless File.directory?(dir)
|
|
79
|
+
|
|
80
|
+
age = heartbeat_age(dir, now)
|
|
81
|
+
next if age <= ORPHAN_TTL_SECONDS
|
|
82
|
+
|
|
83
|
+
"global: #{dir} (session #{name}, last heartbeat #{(age / 3600).floor}h ago)"
|
|
84
|
+
end
|
|
85
|
+
rescue SystemCallError
|
|
86
|
+
[] # unreadable .tmp/: nothing to report, never a crash
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def day_ledger_shape_problems(store_dir)
|
|
90
|
+
root = SessionLedger.sessions_root(store_dir)
|
|
91
|
+
return [] unless File.directory?(root)
|
|
92
|
+
|
|
93
|
+
Dir.children(root).sort.filter_map do |name|
|
|
94
|
+
path = File.join(root, name)
|
|
95
|
+
if !File.directory?(path)
|
|
96
|
+
"global: .sessions/#{name} is not a directory (only day directories belong here)"
|
|
97
|
+
elsif !SessionLedger.valid_day_id?(name)
|
|
98
|
+
"global: .sessions/#{name} is not a YYYYMMDD day id"
|
|
99
|
+
elsif !File.file?(SessionLedger.day_file(store_dir, name))
|
|
100
|
+
"global: .sessions/#{name} has no #{name}.md day file"
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
rescue SystemCallError
|
|
104
|
+
[] # unreadable .sessions/: nothing to report, never a crash
|
|
105
|
+
end
|
|
106
|
+
end
|
|
@@ -3,12 +3,13 @@
|
|
|
3
3
|
|
|
4
4
|
require "open3"
|
|
5
5
|
require_relative "bridge"
|
|
6
|
+
require_relative "arm"
|
|
6
7
|
require_relative "lock"
|
|
7
8
|
require_relative "worktree"
|
|
8
9
|
require_relative "scaffold_intent"
|
|
9
10
|
|
|
10
11
|
# ExecWorktree - all logic for `scripts/exec-worktree` (intent 213, group 2). Finish an
|
|
11
|
-
# intent's code worktree:
|
|
12
|
+
# intent's code worktree: the dirty-worktree guard, then `Worktree.finish` (delivered merges
|
|
12
13
|
# and removes, abandoned removes only).
|
|
13
14
|
#
|
|
14
15
|
# THIS IS THE HIGHEST-RISK SURFACE IN THIS ACTION SET because it composes worktree
|
|
@@ -21,22 +22,13 @@ require_relative "scaffold_intent"
|
|
|
21
22
|
# either of the Lock module's release or takeover operations (that unguarded
|
|
22
23
|
# read-modify-write gap is parked intent 254's territory).
|
|
23
24
|
#
|
|
24
|
-
# THE ORDER-PRECONDITION TRAP (spec D7, binding). `Bridge.code_gate_decision` returns nil
|
|
25
|
-
# UNCONDITIONALLY when the bridge is guided (`build["auto"] != true`), so as a precondition
|
|
26
|
-
# it is VACUOUS in guided mode. This module calls that predicate exactly as it exists,
|
|
27
|
-
# unmodified, and never copies its "reached How" body into a second predicate (the 200/204
|
|
28
|
-
# duplication failure pattern). Instead it states plainly, in the printed output, that on a
|
|
29
|
-
# guided bridge the precondition is advisory only, and that the hook layer
|
|
30
|
-
# (`Bridge.code_gate_decision`, wired through `scripts/lib/edit_gates.rb`) remains the
|
|
31
|
-
# actual enforcement point.
|
|
32
|
-
#
|
|
33
25
|
# Pure and dependency-injected: never calls `exit` or `abort`, never reads `ARGV` directly.
|
|
34
26
|
# Seams: `runner:` (git, defaults to `Worktree::ShellRunner.new`; also used AFTER finish, for
|
|
35
27
|
# a delivered disposition, to verify from committed git state that the merge actually landed,
|
|
36
28
|
# since `Worktree.finish`'s return value is discarded by design and its underlying merge is
|
|
37
29
|
# fail-open, see the merge-verification note below), `finisher:` (defaults to
|
|
38
30
|
# `Worktree.method(:finish)`), `status_checker:` (defaults to a `git status --porcelain`
|
|
39
|
-
# lambda)
|
|
31
|
+
# lambda). Every seam exists so
|
|
40
32
|
# tests run with no real git, no real worktree removal, and no real bridge file outside a
|
|
41
33
|
# tmpdir.
|
|
42
34
|
#
|
|
@@ -58,21 +50,10 @@ module ExecWorktree
|
|
|
58
50
|
|
|
59
51
|
EXIT_OK = 0
|
|
60
52
|
EXIT_USAGE = 1
|
|
61
|
-
|
|
53
|
+
# exit 2 was the order precondition; retired in 2.0 (intent 302) and never reused
|
|
62
54
|
EXIT_NOT_CLEAN = 3
|
|
63
55
|
EXIT_UNRESOLVED = 4
|
|
64
56
|
|
|
65
|
-
PROBE_FILENAME = "exec-worktree-precondition-probe"
|
|
66
|
-
|
|
67
|
-
# The advisory line, verbatim (spec D7): printed whenever the order precondition
|
|
68
|
-
# actually ran (delivered disposition, a code worktree present), on BOTH the pass and
|
|
69
|
-
# the fail path, so no reader mistakes this script for the enforcement point.
|
|
70
|
-
ADVISORY_LINE =
|
|
71
|
-
"This precondition is a friendly early error, not the enforcement point. The hook " \
|
|
72
|
-
"layer (scripts/lib/bridge.rb code_gate_decision, wired through " \
|
|
73
|
-
"scripts/lib/edit_gates.rb) remains the actual gate. On a guided bridge this " \
|
|
74
|
-
"precondition is advisory only."
|
|
75
|
-
|
|
76
57
|
# --- seams (DI defaults) ----------------------------------------------------------
|
|
77
58
|
|
|
78
59
|
# Real `git status --porcelain` against the code worktree, returning the same
|
|
@@ -119,8 +100,7 @@ module ExecWorktree
|
|
|
119
100
|
false
|
|
120
101
|
end
|
|
121
102
|
|
|
122
|
-
# Session resolution order (mirrors scripts/end-intent:418-426,
|
|
123
|
-
# scripts/start-intent's no-fallback order): explicit --session, else
|
|
103
|
+
# Session resolution order (mirrors scripts/end-intent:418-426): explicit --session, else
|
|
124
104
|
# CLAUDE_CODE_SESSION_ID (passed in as env_session, never read from ENV here), else
|
|
125
105
|
# the existing delivery.lock's own recorded owner_session when non-blank, else nil.
|
|
126
106
|
# The lock-owner fallback is correct here: this is a teardown step on an intent this
|
|
@@ -134,7 +114,7 @@ module ExecWorktree
|
|
|
134
114
|
Bridge.blank?(owner) ? nil : owner
|
|
135
115
|
end
|
|
136
116
|
|
|
137
|
-
# --- result builders (
|
|
117
|
+
# --- result builders (the exit_code/stdout/stderr shape the thin CLIs share) --------
|
|
138
118
|
|
|
139
119
|
def usage_result(message)
|
|
140
120
|
{ exit_code: EXIT_USAGE, stderr: ["exec-worktree: #{message}"], stdout: [] }
|
|
@@ -156,15 +136,9 @@ module ExecWorktree
|
|
|
156
136
|
"recorded owner are all blank); refusing rather than guessing."
|
|
157
137
|
end
|
|
158
138
|
|
|
159
|
-
def no_bridge_message(intent_dir, session, id)
|
|
160
|
-
"exec-worktree: no bridge resolved for session #{session.inspect} and intent " \
|
|
161
|
-
"#{id.inspect} (#{File.basename(intent_dir)}); any worktree this intent provisioned " \
|
|
162
|
-
"was NOT removed. Run /plastic-doctor to check for an orphaned worktree."
|
|
163
|
-
end
|
|
164
|
-
|
|
165
139
|
def nothing_provisioned_message(intent_dir)
|
|
166
|
-
"exec-worktree: #{File.basename(intent_dir)} has no code worktree
|
|
167
|
-
"
|
|
140
|
+
"exec-worktree: #{File.basename(intent_dir)} has no code worktree on disk " \
|
|
141
|
+
"(projects.yml and the intent id resolve none); nothing was provisioned, nothing " \
|
|
168
142
|
"to finish."
|
|
169
143
|
end
|
|
170
144
|
|
|
@@ -195,7 +169,7 @@ module ExecWorktree
|
|
|
195
169
|
|
|
196
170
|
# --- report (printed only when the run reaches a normal finish) ------------------
|
|
197
171
|
|
|
198
|
-
def build_report(intent_dir:, disposition:,
|
|
172
|
+
def build_report(intent_dir:, disposition:, worktree_code:, branch:,
|
|
199
173
|
target_branch:, removed:)
|
|
200
174
|
# This is only ever reached for "delivered" AFTER `run` has already confirmed, by
|
|
201
175
|
# reading committed git state (merged_into_current_branch?), that `branch` IS an
|
|
@@ -215,7 +189,6 @@ module ExecWorktree
|
|
|
215
189
|
[
|
|
216
190
|
"exec-worktree: #{File.basename(intent_dir)}",
|
|
217
191
|
" disposition: #{disposition}",
|
|
218
|
-
" precondition: #{precondition_status}",
|
|
219
192
|
" worktree: #{worktree_code}",
|
|
220
193
|
" branch: #{branch}",
|
|
221
194
|
" merge: #{merge_line}",
|
|
@@ -228,8 +201,7 @@ module ExecWorktree
|
|
|
228
201
|
def run(store:, id:, home:, disposition:, session:, env_session:,
|
|
229
202
|
runner: Worktree::ShellRunner.new,
|
|
230
203
|
finisher: Worktree.method(:finish),
|
|
231
|
-
status_checker: default_status_checker
|
|
232
|
-
gate: Bridge.method(:code_gate_decision))
|
|
204
|
+
status_checker: default_status_checker)
|
|
233
205
|
return usage_result("--store is required") if Bridge.blank?(store)
|
|
234
206
|
return usage_result("--id is required") if Bridge.blank?(id)
|
|
235
207
|
return usage_result("--home is required") if Bridge.blank?(home)
|
|
@@ -252,9 +224,7 @@ module ExecWorktree
|
|
|
252
224
|
return deny_result(EXIT_UNRESOLVED, no_session_message(intent_dir))
|
|
253
225
|
end
|
|
254
226
|
|
|
255
|
-
bridge_data =
|
|
256
|
-
return ok_result(no_bridge_message(intent_dir, key_session, id)) if bridge_data.nil?
|
|
257
|
-
|
|
227
|
+
bridge_data = Arm.bridge_hash(intent_dir: intent_dir, home: normalized_home)
|
|
258
228
|
worktree_code = bridge_data.dig("worktree", "code")
|
|
259
229
|
return ok_result(nothing_provisioned_message(intent_dir)) if Bridge.blank?(worktree_code)
|
|
260
230
|
|
|
@@ -264,22 +234,8 @@ module ExecWorktree
|
|
|
264
234
|
# both `repo` and `branch` to work with.
|
|
265
235
|
repo = repo_from_worktree_code(worktree_code)
|
|
266
236
|
|
|
267
|
-
precondition_ran = false
|
|
268
|
-
precondition_status = "not run (abandoned)"
|
|
269
|
-
|
|
270
237
|
if disposition == "delivered"
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
# Step 1: the order precondition. Call Bridge.code_gate_decision AS-IS (never
|
|
274
|
-
# relaxed, never duplicated). See the module doc's "order-precondition trap".
|
|
275
|
-
probe_path = File.join(worktree_code, PROBE_FILENAME)
|
|
276
|
-
reason = gate.call(bridge_data, probe_path, home: normalized_home)
|
|
277
|
-
if reason
|
|
278
|
-
return deny_result(EXIT_PRECONDITION, [reason, ADVISORY_LINE])
|
|
279
|
-
end
|
|
280
|
-
precondition_status = bridge_data.dig("build", "auto") == true ? "passed" : "advisory (guided bridge)"
|
|
281
|
-
|
|
282
|
-
# Step 2: the dirty-worktree guard, delivered only. Fail CLOSED when the check
|
|
238
|
+
# Step 1: the dirty-worktree guard, delivered only. Fail CLOSED when the check
|
|
283
239
|
# itself fails or raises: removal force-removes on a plain failure, which would
|
|
284
240
|
# destroy uncommitted work an inconclusive check could not rule out.
|
|
285
241
|
out, err, status = status_checker.call(worktree_code)
|
|
@@ -290,7 +246,7 @@ module ExecWorktree
|
|
|
290
246
|
end
|
|
291
247
|
end
|
|
292
248
|
|
|
293
|
-
# Step
|
|
249
|
+
# Step 2: finish. Worktree.finish is fail-open and never raises: on a merge conflict it
|
|
294
250
|
# aborts the merge, warns, and STILL removes the worktree (Worktree.merge_branch,
|
|
295
251
|
# scripts/lib/worktree.rb:246-258). So a failed merge is invisible both in this call's
|
|
296
252
|
# return value (discarded, as before) and in whether the worktree directory still
|
|
@@ -315,11 +271,9 @@ module ExecWorktree
|
|
|
315
271
|
end
|
|
316
272
|
|
|
317
273
|
report = build_report(intent_dir: intent_dir, disposition: disposition,
|
|
318
|
-
precondition_status: precondition_status,
|
|
319
274
|
worktree_code: worktree_code, branch: branch,
|
|
320
275
|
target_branch: target_branch, removed: removed)
|
|
321
276
|
|
|
322
|
-
|
|
323
|
-
ok_result(stdout)
|
|
277
|
+
ok_result([report])
|
|
324
278
|
end
|
|
325
279
|
end
|
|
@@ -19,9 +19,10 @@ module HarnessText
|
|
|
19
19
|
# form the install_claude hook rewrite already writes.
|
|
20
20
|
PLUGIN_ROOT_CODEX = "$HOME/.plastic"
|
|
21
21
|
|
|
22
|
-
# Spec D3:
|
|
23
|
-
#
|
|
24
|
-
|
|
22
|
+
# Spec D3: files that document Claude's placeholder convention TO SKILL AUTHORS are
|
|
23
|
+
# exempt by path. The skill-authoring reference left the installed tree in 2.0
|
|
24
|
+
# (intent 304), so the list is empty; the seam stays for the next such file.
|
|
25
|
+
PLUGIN_ROOT_EXEMPT = [].freeze
|
|
25
26
|
|
|
26
27
|
# Spec D4: an explicit root table, not one regex. The ~/.claude lines in the skill
|
|
27
28
|
# corpus carry four different meanings; only these three have an exact Codex
|
|
@@ -46,7 +47,7 @@ module HarnessText
|
|
|
46
47
|
# "~/.claude/hooks/plastic-session-start");
|
|
47
48
|
# the name alternation, built from the real skills/ directory listing, rejects
|
|
48
49
|
# anything that is not an installed skill ("/plastic-lock" is a CLI script,
|
|
49
|
-
# "/plastic-
|
|
50
|
+
# "/plastic-enforcer" is an agent role).
|
|
50
51
|
# Longest name first so /plastic-intent-continuing cannot match "continuing".
|
|
51
52
|
def rewrite_skill_prefix(text, skill_names, prefix)
|
|
52
53
|
return text if skill_names.nil? || skill_names.empty?
|