@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.
Files changed (190) hide show
  1. package/PLASTIC.md +101 -294
  2. package/README.md +5 -6
  3. package/agents/plastic-advisor.md +18 -17
  4. package/agents/plastic-enforcer.md +57 -69
  5. package/agents/plastic-executor.md +34 -20
  6. package/agents/plastic-faux-advisor.md +18 -18
  7. package/config_asks.yml +38 -0
  8. package/hooks/capture +4 -0
  9. package/hooks/check-update +12 -3
  10. package/hooks/close +5 -0
  11. package/hooks/hooks.json +9 -49
  12. package/hooks/record +4 -0
  13. package/package.json +3 -2
  14. package/scripts/agent-report +8 -9
  15. package/scripts/append-ledger +195 -0
  16. package/scripts/codex-hook +101 -95
  17. package/scripts/dashboard.rb +4 -4
  18. package/scripts/doctor.rb +106 -43
  19. package/scripts/end-intent +77 -76
  20. package/scripts/exec-worktree +11 -20
  21. package/scripts/file-session-intent +61 -0
  22. package/scripts/hook-capture +242 -0
  23. package/scripts/hook-close +34 -0
  24. package/scripts/hook-record +149 -0
  25. package/scripts/hook-session-start +95 -12
  26. package/scripts/install.rb +8 -6
  27. package/scripts/lib/agent_models.rb +1 -7
  28. package/scripts/lib/arm.rb +283 -0
  29. package/scripts/lib/backfill_intent.rb +316 -0
  30. package/scripts/lib/bridge.rb +24 -1597
  31. package/scripts/lib/doctor_core.rb +17 -88
  32. package/scripts/lib/doctor_session_ledger.rb +106 -0
  33. package/scripts/lib/exec_worktree.rb +14 -60
  34. package/scripts/lib/harness_text.rb +5 -4
  35. package/scripts/lib/hook_registry.rb +44 -107
  36. package/scripts/lib/insights.rb +3 -4
  37. package/scripts/lib/installer_core.rb +19 -30
  38. package/scripts/lib/lock.rb +6 -26
  39. package/scripts/lib/outcome_guard.rb +7 -5
  40. package/scripts/lib/project_validator.rb +30 -1
  41. package/scripts/lib/roadmap_queue.rb +3 -3
  42. package/scripts/lib/roadmap_savepoint.rb +1 -1
  43. package/scripts/lib/rule_catalog.rb +6 -3
  44. package/scripts/lib/savepoint.rb +320 -0
  45. package/scripts/lib/scaffold_intent.rb +13 -179
  46. package/scripts/lib/session_backfill.rb +184 -0
  47. package/scripts/lib/session_close.rb +84 -0
  48. package/scripts/lib/session_git.rb +469 -0
  49. package/scripts/lib/session_ledger.rb +497 -0
  50. package/scripts/lib/store_discovery.rb +1 -1
  51. package/scripts/lib/store_provisioning.rb +1 -1
  52. package/scripts/maintenance-run +7 -7
  53. package/scripts/new-intent +63 -4
  54. package/scripts/plastic-lock +94 -41
  55. package/scripts/promote-session-item +150 -0
  56. package/scripts/rebuild-graph +1 -1
  57. package/scripts/roadmap-next +2 -2
  58. package/scripts/rollback.rb +42 -3
  59. package/scripts/scaffold-intent +37 -63
  60. package/scripts/session-commit +116 -0
  61. package/scripts/spawn-preamble +5 -5
  62. package/scripts/update.rb +7 -3
  63. package/skills/agent-advisor/SKILL.md +3 -3
  64. package/skills/agent-advisor/references/advisor-protocol.md +20 -20
  65. package/skills/auto/SKILL.md +218 -317
  66. package/skills/auto/evals/evals.json +10 -10
  67. package/skills/auto/references/agent-architecture.md +102 -90
  68. package/skills/auto/references/agent-report-contract.md +4 -21
  69. package/skills/auto/references/end-tail.md +47 -53
  70. package/skills/auto/references/human-report-contract.md +8 -8
  71. package/skills/conventions/SKILL.md +1 -3
  72. package/skills/conventions/references/lifecycle-and-savepoints.md +35 -1
  73. package/skills/conventions/references/locks-and-worktrees.md +47 -55
  74. package/skills/conventions/references/maintenance-and-revisions.md +12 -12
  75. package/skills/dashboard/SKILL.md +1 -1
  76. package/skills/direct/SKILL.md +66 -0
  77. package/skills/direct/references/request-signals.md +59 -0
  78. package/skills/doctor/SKILL.md +58 -11
  79. package/skills/doctor/report.md +3 -1
  80. package/skills/install/SKILL.md +11 -9
  81. package/skills/intent-continuing/SKILL.md +131 -89
  82. package/skills/intent-continuing/references/boarding-matrix.md +34 -0
  83. package/skills/intent-continuing/references/context-management.md +2 -2
  84. package/skills/intent-creating/SKILL.md +2 -2
  85. package/skills/intent-creating/references/lifecycle.md +1 -1
  86. package/skills/intent-ending/SKILL.md +28 -37
  87. package/skills/intent-ending/evals/evals.json +4 -4
  88. package/skills/intent-executing/SKILL.md +12 -49
  89. package/skills/intent-executing/plan-reviewer-prompt.md +39 -0
  90. package/skills/intent-speccing/SKILL.md +108 -51
  91. package/skills/intent-speccing/evals/evals.json +5 -5
  92. package/skills/intent-speccing/references/per-section-fill-rules.md +4 -4
  93. package/skills/intent-speccing/references/self-verify-checklist.md +1 -2
  94. package/skills/project-creating/SKILL.md +1 -1
  95. package/skills/releasing/SKILL.md +7 -8
  96. package/skills/tutorial/SKILL.md +2 -2
  97. package/skills/tutorial/references/track-1-guided.md +16 -18
  98. package/skills/tutorial/references/track-2-auto.md +12 -13
  99. package/skills/tutorial/references/track-3-projects-and-roadmaps.md +2 -2
  100. package/templates/agents.md +4 -7
  101. package/templates/project.yml +7 -0
  102. package/templates/roadmap.md +1 -1
  103. package/templates/savepoint.md +2 -2
  104. package/templates/session-intent.md +29 -0
  105. package/agents/plastic-brainstorming.md +0 -48
  106. package/agents/plastic-future-intent-researcher.md +0 -35
  107. package/agents/plastic-intent-curator.md +0 -47
  108. package/agents/plastic-intent-discovery.md +0 -43
  109. package/agents/plastic-planner.md +0 -56
  110. package/agents/plastic-spec-specialist.md +0 -53
  111. package/hooks/auto-arm +0 -5
  112. package/hooks/bash-gate +0 -3
  113. package/hooks/continue +0 -31
  114. package/hooks/edit-gates +0 -3
  115. package/hooks/future-intent-check +0 -25
  116. package/hooks/gate-check +0 -12
  117. package/hooks/power-tools +0 -8
  118. package/scripts/hook-auto-arm +0 -51
  119. package/scripts/hook-bash-gate +0 -78
  120. package/scripts/hook-code-gate +0 -38
  121. package/scripts/hook-continue +0 -48
  122. package/scripts/hook-create-gate +0 -51
  123. package/scripts/hook-edit-gates +0 -58
  124. package/scripts/hook-future-intent-check +0 -90
  125. package/scripts/hook-gate-check +0 -169
  126. package/scripts/hook-links-gate +0 -54
  127. package/scripts/hook-lock-gate +0 -47
  128. package/scripts/hook-power-tools +0 -38
  129. package/scripts/hook-savepoint-pre +0 -32
  130. package/scripts/lib/codex_edit_gates.rb +0 -138
  131. package/scripts/lib/edit_gates.rb +0 -398
  132. package/scripts/lib/links_gate.rb +0 -140
  133. package/scripts/lib/qmd_hook.rb +0 -41
  134. package/scripts/lib/spec_header.rb +0 -83
  135. package/scripts/lib/start_intent.rb +0 -296
  136. package/scripts/start-intent +0 -89
  137. package/skills/_active-intent-gate.md +0 -26
  138. package/skills/auto/references/tiers.md +0 -100
  139. package/skills/continuing/SKILL.md +0 -34
  140. package/skills/continuing/evals/evals.json +0 -91
  141. package/skills/conventions/references/gates-and-enforcement.md +0 -53
  142. package/skills/conventions/references/tiers-and-dispatch.md +0 -135
  143. package/skills/doctor/references/gates-stuck-detection.md +0 -43
  144. package/skills/intent-brainstorming/SKILL.md +0 -118
  145. package/skills/intent-brainstorming/evals/evals.json +0 -67
  146. package/skills/intent-continuing/evals/evals.json +0 -145
  147. package/skills/intent-discovering/SKILL.md +0 -56
  148. package/skills/intent-grilling/SKILL.md +0 -108
  149. package/skills/intent-linking/SKILL.md +0 -128
  150. package/skills/intent-linking/evals/evals.json +0 -22
  151. package/skills/intent-linking/references/zettelkasten.md +0 -45
  152. package/skills/intent-locking/SKILL.md +0 -64
  153. package/skills/intent-planning/SKILL.md +0 -145
  154. package/skills/intent-planning/evals/evals.json +0 -81
  155. package/skills/intent-planning/references/plan-format.md +0 -130
  156. package/skills/intent-researching/SKILL.md +0 -123
  157. package/skills/intent-researching/evals/evals.json +0 -22
  158. package/skills/intent-savepoint/SKILL.md +0 -83
  159. package/skills/intent-savepoint/references/context-management.md +0 -32
  160. package/skills/intent-starting/SKILL.md +0 -151
  161. package/skills/intent-starting/evals/evals.json +0 -117
  162. package/skills/intent-starting/references/boarding-matrix.md +0 -35
  163. package/skills/project-continuing/SKILL.md +0 -119
  164. package/skills/project-continuing/evals/evals.json +0 -100
  165. package/skills/roadmap-continuing/SKILL.md +0 -89
  166. package/skills/roadmap-continuing/evals/evals.json +0 -82
  167. package/skills/skill-creating/SKILL.md +0 -75
  168. package/skills/skill-creating/evals/evals.json +0 -108
  169. package/skills/skill-creating/references/agents.md +0 -168
  170. package/skills/skill-creating/references/defaults-first.md +0 -23
  171. package/skills/skill-creating/references/evals.md +0 -41
  172. package/skills/skill-creating/references/hooks.md +0 -251
  173. package/skills/skill-creating/references/progressive-disclosure.md +0 -176
  174. package/skills/skill-creating/references/scripts.md +0 -166
  175. package/skills/skill-creating/references/skills.md +0 -169
  176. package/skills/skill-creating/scripts/scaffold.rb +0 -313
  177. package/skills/skill-evaluating/SKILL.md +0 -141
  178. package/skills/skill-evaluating/assets/eval-template.json +0 -12
  179. package/skills/skill-evaluating/evals/evals.json +0 -75
  180. package/skills/skill-evaluating/references/convention-checks.md +0 -76
  181. package/skills/skill-evaluating/references/eval-methodology.md +0 -154
  182. package/skills/store-curating/SKILL.md +0 -64
  183. package/skills/store-curating/evals/evals.json +0 -38
  184. package/skills/store-indexing/SKILL.md +0 -93
  185. package/skills/store-indexing/evals/evals.json +0 -22
  186. package/skills/store-indexing/references/zettelkasten-linking.md +0 -32
  187. package/skills/store-provisioning/SKILL.md +0 -55
  188. /package/skills/{project-continuing → intent-continuing}/references/board-fill.md +0 -0
  189. /package/skills/{roadmap-continuing → intent-continuing}/references/liveness-ranking.md +0 -0
  190. /package/skills/{intent-brainstorming → intent-speccing}/references/design-principles.md +0 -0
@@ -10,9 +10,8 @@
10
10
  module HookRegistry
11
11
  module_function
12
12
 
13
- # MCP tools that MUTATE files. Every write/lock/create gate must match them,
14
- # or symbolic edits bypass the whole gate layer (the universal MCP-edit
15
- # bypass found in 108's gate inventory).
13
+ # MCP tools that MUTATE files. The record hook must match them, or a symbolic
14
+ # edit never reaches the savepoint ledger or the day ledger.
16
15
  SERENA_EDIT_TOOLS = %w[
17
16
  mcp__serena__replace_content
18
17
  mcp__serena__replace_symbol_body
@@ -24,40 +23,6 @@ module HookRegistry
24
23
 
25
24
  WRITE_MATCHER = (%w[Write Edit NotebookEdit] + SERENA_EDIT_TOOLS).join("|")
26
25
 
27
- # Per-gate tool applicability for the merged edit-path dispatcher (intent 244,
28
- # spec D-d/D-l). Registration collapses to ONE PreToolUse hook on WRITE_MATCHER
29
- # (a strict superset of the three former matcher groups), and this table is what
30
- # keeps each gate's own coverage exactly what it was: scripts/hook-edit-gates
31
- # reads tool_name off the payload and skips any gate whose list excludes it.
32
- # Key order IS the evaluation order (spec D-a): savepoint-pre first because it
33
- # never denies and its ledger append must stay unconditional; lock-gate leads
34
- # the deniers because holding the delivery lock is the precondition the other
35
- # rules assume. A test pins this table against today's three matcher groups, so
36
- # no gate's coverage can widen or narrow silently.
37
- GATE_TOOLS = {
38
- "savepoint-pre" => %w[Write Edit].freeze,
39
- "lock-gate" => (%w[Write Edit NotebookEdit] + SERENA_EDIT_TOOLS).freeze,
40
- "code-gate" => (%w[Write Edit NotebookEdit] + SERENA_EDIT_TOOLS).freeze,
41
- "links-gate" => %w[Write Edit].freeze,
42
- "create-gate" => (%w[Write Edit] + SERENA_EDIT_TOOLS).freeze,
43
- }.freeze
44
-
45
- # Per-gate tool applicability for the merged CODEX edit-path dispatcher (intent
46
- # 251, spec D1). Codex reports exactly one file-mutation tool name, apply_patch
47
- # (intent 181 F4), so every value is the same single-entry list. The table is
48
- # NOT decoration: EditGates.tool_applies? builds a regex from these values, and
49
- # reusing GATE_TOOLS here would match none of them against "apply_patch" and
50
- # silently skip all five gates. Key order IS the evaluation order and is the
51
- # SAME order GATE_TOOLS uses (spec D5), so the two harnesses evaluate the five
52
- # gates in one order, not two.
53
- CODEX_GATE_TOOLS = {
54
- "savepoint-pre" => %w[apply_patch].freeze,
55
- "lock-gate" => %w[apply_patch].freeze,
56
- "code-gate" => %w[apply_patch].freeze,
57
- "links-gate" => %w[apply_patch].freeze,
58
- "create-gate" => %w[apply_patch].freeze,
59
- }.freeze
60
-
61
26
  # event => ordered list of { "matcher" =>, "hooks" => [{ "name" =>, "status" => }] }
62
27
  # The name is the hooks/<name> launcher; the flat install ships it as
63
28
  # ~/.claude/hooks/plastic-<name>.
@@ -74,75 +39,54 @@ module HookRegistry
74
39
  { "name" => "savepoint", "status" => "Saving Plastic intent state..." },
75
40
  ] },
76
41
  ],
77
- "PreToolUse" => [
78
- # ONE registered edit-path hook (intent 244, spec D-d). WRITE_MATCHER is a
79
- # strict superset of the three matcher groups this replaces; per-gate tool
80
- # applicability lives in GATE_TOOLS above and is read by
81
- # scripts/hook-edit-gates, so no gate's coverage widened or narrowed.
42
+ "PostToolUse" => [
82
43
  { "matcher" => WRITE_MATCHER, "hooks" => [
83
- { "name" => "edit-gates", "status" => "Checking Plastic gates..." },
84
- ] },
85
- { "matcher" => "Bash", "hooks" => [
86
- { "name" => "bash-gate", "status" => "Checking lifecycle gate..." },
44
+ { "name" => "record", "status" => "Recording Plastic session state..." },
87
45
  ] },
88
46
  ],
89
- "PostToolUse" => [
90
- { "matcher" => "Write|Edit", "hooks" => [
91
- { "name" => "gate-check", "status" => "Checking lifecycle gates..." },
47
+ "SessionEnd" => [
48
+ { "matcher" => "", "hooks" => [
49
+ { "name" => "close", "status" => "Closing the Plastic session..." },
92
50
  ] },
93
51
  ],
94
52
  "UserPromptSubmit" => [
95
53
  { "matcher" => "", "hooks" => [
96
- { "name" => "continue", "status" => "Checking for continue..." },
97
- { "name" => "future-intent-check", "status" => "Checking future intents..." },
98
- { "name" => "auto-arm", "status" => "Checking auto mode..." },
99
- { "name" => "power-tools", "status" => "Checking power tools..." },
54
+ { "name" => "capture", "status" => "Capturing prompt into the session ledger..." },
100
55
  ] },
101
56
  ],
102
57
  }
103
58
  end
104
59
 
105
60
  # Codex registration (~/.codex/hooks.json, intent 102). Derived from `events`:
106
- # the file-mutation PreToolUse gate/savepoint hooks collapse from Claude's
107
- # multi-tool matchers onto Codex's single apply_patch tool (181 F4: apply_patch
108
- # is Codex's sole file-mutation tool; tool_name always reports apply_patch), plus
109
- # the PostToolUse gate-check. Command invokes the codex-hook dispatcher with the
110
- # gate name. Guide-settled shape [guide Part 3]: top-level {"hooks":{<Event>:
111
- # [{"matcher","hooks":[{"type":"command","command","statusMessage"}]}]}},
112
- # identical to Claude's shape, string command. Single source of truth (108 D7):
113
- # any drift from `events` is a bug, pinned by test.
114
- #
115
- # Codex's apply_patch matcher carries ONE dispatcher command, edit-gates,
116
- # which runs all five gates in-process through scripts/lib/codex_edit_gates.rb
117
- # (intent 251), mirroring what intent 244 did for Claude. Five separately
118
- # registered commands used to cost eight OS processes per edit (five
119
- # top-level plus three nested run_core children); one dispatcher process
120
- # reaches the same five gate decisions.
121
- CODEX_PRE_HOOKS = %w[edit-gates].freeze
122
- CODEX_POST_HOOKS = %w[gate-check].freeze
123
-
124
- # Codex's shell-tool gate hole (intent 203): bash-gate (denies a shell write to
125
- # project code before How) belongs on the Bash matcher, and ONLY Bash: the
126
- # official Codex hooks doc's PreToolUse event catalog enumerates exactly Bash,
127
- # apply_patch, and MCP tool calls, and neither it nor the two prior Codex
128
- # research passes (198's official-docs research, 181's deep research)
129
- # documents a discrete Read, Grep, or Glob tool name (D3). Registering a tool
130
- # name Codex never reports would be dead weight that looks alive, the exact
131
- # defect this intent exists to fix.
132
- CODEX_BASH_HOOKS = %w[bash-gate].freeze
133
-
134
- # Live-state events registered WHOLE (intent 199), unlike CODEX_PRE_HOOKS/
135
- # CODEX_POST_HOOKS above: Codex's SessionStart/UserPromptSubmit/PreCompact already
136
- # match Claude's shape exactly, one matcher group each ("", no tool to collapse
137
- # onto), so every hook `events` lists under these three events projects straight
138
- # through with no allowlist to keep in sync. A hook added to any of them on the
139
- # Claude side registers for Codex automatically.
61
+ # the PostToolUse record hook collapses from Claude's multi-tool matcher onto
62
+ # Codex's single apply_patch tool (181 F4: apply_patch is Codex's sole
63
+ # file-mutation tool; tool_name always reports apply_patch), and the live-state
64
+ # events project through whole. Since intent 302 there is no PreToolUse group at
65
+ # all: the edit-path gates are gone on both harnesses. Command invokes the
66
+ # codex-hook dispatcher with the hook name. Guide-settled shape [guide Part 3]:
67
+ # top-level {"hooks":{<Event>: [{"matcher","hooks":[{"type":"command","command",
68
+ # "statusMessage"}]}]}}, identical to Claude's shape, string command. Single
69
+ # source of truth (108 D7): any drift from `events` is a bug, pinned by test.
70
+ CODEX_POST_HOOKS = %w[record].freeze
71
+
72
+ # Live-state events registered WHOLE (intent 199): Codex's SessionStart/
73
+ # UserPromptSubmit/PreCompact already match Claude's shape exactly, one matcher
74
+ # group each ("", no tool to collapse onto), so every hook `events` lists under
75
+ # these three events projects straight through with no allowlist to keep in
76
+ # sync. A hook added to any of them on the Claude side registers for Codex
77
+ # automatically.
140
78
  CODEX_LIVE_STATE_EVENTS = %w[SessionStart UserPromptSubmit PreCompact].freeze
141
79
 
80
+ # SessionEnd on Codex (intent 309): projected from its own constant, never as a fourth
81
+ # live-state event, because the dispatch differs. Codex kills a SessionEnd hook after
82
+ # 3 seconds, so scripts/codex-hook hands `close` to its launcher detached and returns at
83
+ # once instead of the synchronous relay the live-state hooks get. Codex ships both Stop
84
+ # and SessionEnd (codex-rs/hooks at rust-v0.149.1); the design uses SessionEnd on both
85
+ # harnesses. Like STATE_HOOKS, the dispatcher's list is hand-kept and cross-checked by
86
+ # test/hook_registry_test.rb.
87
+ CODEX_SESSION_END_HOOKS = %w[close].freeze
88
+
142
89
  def codex_hooks_json(dispatcher_path:)
143
- # Every Codex hook name is now a name `events` itself carries (intent 251,
144
- # spec D9): edit-gates is Claude's own PreToolUse hook name, so there is
145
- # nothing left to merge in from a separate status table.
146
90
  status_by_name = events.values.flatten.flat_map { |g| g["hooks"] }
147
91
  .each_with_object({}) { |h, m| m[h["name"]] = h["status"] }
148
92
  cmd = ->(name) {
@@ -150,27 +94,19 @@ module HookRegistry
150
94
  "command" => "\"#{dispatcher_path}\" #{name}",
151
95
  "statusMessage" => status_by_name[name].to_s }
152
96
  }
153
- # Preserve the order these hook names appear across the PreToolUse groups in `events`.
154
- pre_order = events["PreToolUse"].flat_map { |g| g["hooks"].map { |h| h["name"] } }
155
- # The Codex apply_patch matcher carries the same edit-gates name Claude's own
156
- # PreToolUse group carries (intent 251, spec D9), so the intersection with
157
- # pre_order validates the Codex gate name against the single source of truth,
158
- # `events`, rather than against a table of Claude tool applicability.
159
- pre = (CODEX_PRE_HOOKS & pre_order).map { |n| cmd.call(n) }
160
- bash = (pre_order & CODEX_BASH_HOOKS).map { |n| cmd.call(n) }
161
- post = CODEX_POST_HOOKS.map { |n| cmd.call(n) }
97
+ # Validate the Codex name against the single source of truth, `events`.
98
+ post_order = events["PostToolUse"].flat_map { |g| g["hooks"].map { |h| h["name"] } }
99
+ post = (CODEX_POST_HOOKS & post_order).map { |n| cmd.call(n) }
162
100
 
163
101
  result = {
164
- "PreToolUse" => [
165
- { "matcher" => "apply_patch", "hooks" => pre },
166
- { "matcher" => "Bash", "hooks" => bash },
167
- ],
168
102
  "PostToolUse" => [{ "matcher" => "apply_patch", "hooks" => post }],
169
103
  }
170
104
  CODEX_LIVE_STATE_EVENTS.each do |event|
171
105
  names = events[event].flat_map { |g| g["hooks"].map { |h| h["name"] } }
172
106
  result[event] = [{ "matcher" => "", "hooks" => names.map { |n| cmd.call(n) } }]
173
107
  end
108
+ end_order = events["SessionEnd"].flat_map { |g| g["hooks"].map { |h| h["name"] } }
109
+ result["SessionEnd"] = [{ "matcher" => "", "hooks" => (CODEX_SESSION_END_HOOKS & end_order).map { |n| cmd.call(n) } }]
174
110
  result
175
111
  end
176
112
 
@@ -179,8 +115,7 @@ module HookRegistry
179
115
  # installed as ~/.claude/hooks/plastic-<name>. The single derivation doctor's
180
116
  # hooks_exist/hooks_executable/hooks_no_orphans checks read from, so a
181
117
  # hand-kept list of launchers can never drift out of step with `events`
182
- # again (the gap that let 8 of 15 launchers, all the enforcement gates, go
183
- # unchecked).
118
+ # again (the gap that once let 8 of 15 launchers go unchecked).
184
119
  def claude_launcher_names
185
120
  events.values.flatten.flat_map { |g| g["hooks"].map { |h| h["name"] } }
186
121
  .uniq.sort.map { |name| "plastic-#{name}" }
@@ -204,8 +139,10 @@ module HookRegistry
204
139
  # hooks_exist demands be present on disk, so a retired name there makes a correct
205
140
  # install report missing launchers.
206
141
  RETIRED_HOOK_NAMES = %w[
207
- code-gate create-gate links-gate lock-gate savepoint-pre
142
+ edit-gates bash-gate code-gate create-gate links-gate lock-gate savepoint-pre
208
143
  qmd-search retrieval-gate model-instructions opus-manual
144
+ continue future-intent-check auto-arm gate-check
145
+ power-tools
209
146
  ].freeze
210
147
 
211
148
  RETIRED_CLAUDE_LAUNCHERS = RETIRED_HOOK_NAMES.map { |n| "plastic-#{n}" }.freeze
@@ -226,7 +163,7 @@ module HookRegistry
226
163
  live = CODEX_LIVE_STATE_EVENTS.flat_map do |event|
227
164
  events[event].flat_map { |g| g["hooks"].map { |h| h["name"] } }
228
165
  end
229
- (CODEX_PRE_HOOKS + CODEX_POST_HOOKS + CODEX_BASH_HOOKS + live).uniq.sort
166
+ (CODEX_POST_HOOKS + live + CODEX_SESSION_END_HOOKS).uniq.sort
230
167
  end
231
168
 
232
169
  def codex_purgeable_hook_names
@@ -7,14 +7,13 @@
7
7
  #
8
8
  # Every entry leads with a fixed, machine-parseable prefix
9
9
  # `{utc-iso8601} · {stage} · {author}` where the timestamp is `now.utc.iso8601`,
10
- # the SAME convention `Bridge.append_savepoint_line` uses, so the store has one
10
+ # the SAME convention `Savepoint.append_savepoint_line` uses, so the store has one
11
11
  # timestamp convention across both ledgers. Entries are append-only, newest at
12
12
  # the BOTTOM (the [[34]] ordering law): the per-entry prefix is not prepending
13
13
  # the entry, and existing entries are never reordered.
14
14
 
15
15
  require "time"
16
- require_relative "bridge"
17
-
16
+ require_relative "savepoint"
18
17
  module Insights
19
18
  HEADING = "## Insights"
20
19
  # The middle dot (U+00B7) with a single space on each side separates the
@@ -35,7 +34,7 @@ module Insights
35
34
  end
36
35
 
37
36
  entry = "#{prefix} — #{text}"
38
- path = Bridge.intent_file(intent_dir)
37
+ path = Savepoint.intent_file(intent_dir)
39
38
  File.write(path, with_entry(File.exist?(path) ? File.read(path) : "", entry))
40
39
  entry
41
40
  end
@@ -174,13 +174,6 @@ class InstallerCore
174
174
  keys.uniq
175
175
  end
176
176
 
177
- def channel_from(argv, default: "latest")
178
- return "alpha" if argv.include?("--alpha")
179
- return "beta" if argv.include?("--beta")
180
- return "latest" if argv.include?("--latest")
181
- default
182
- end
183
-
184
177
  def prompt_agents(input: $stdin)
185
178
  return ["claude"] unless input.tty?
186
179
 
@@ -329,22 +322,17 @@ class InstallerCore
329
322
  "scripts/write-config" => "scripts/write-config",
330
323
  "scripts/select-update-target" => "scripts/select-update-target",
331
324
  "scripts/hook-session-start" => "scripts/hook-session-start",
332
- "scripts/hook-continue" => "scripts/hook-continue",
333
- "scripts/hook-future-intent-check" => "scripts/hook-future-intent-check",
334
- "scripts/hook-gate-check" => "scripts/hook-gate-check",
335
- "scripts/hook-savepoint-pre" => "scripts/hook-savepoint-pre",
336
- "scripts/hook-power-tools" => "scripts/hook-power-tools",
337
- "scripts/lib/qmd_hook.rb" => "scripts/lib/qmd_hook.rb",
325
+ "scripts/hook-capture" => "scripts/hook-capture",
326
+ "scripts/hook-record" => "scripts/hook-record",
327
+ "scripts/hook-close" => "scripts/hook-close",
338
328
  "scripts/lib/power_tools.rb" => "scripts/lib/power_tools.rb",
339
329
  "scripts/lib/ruby_probe.rb" => "scripts/lib/ruby_probe.rb",
340
330
  "scripts/lib/agent_models.rb" => "scripts/lib/agent_models.rb",
341
331
  "scripts/lib/config_asks.rb" => "scripts/lib/config_asks.rb",
342
332
  "scripts/lib/release_guard.rb" => "scripts/lib/release_guard.rb",
343
- "scripts/hook-code-gate" => "scripts/hook-code-gate",
344
- "scripts/hook-lock-gate" => "scripts/hook-lock-gate",
345
- "scripts/hook-bash-gate" => "scripts/hook-bash-gate",
346
- "scripts/hook-auto-arm" => "scripts/hook-auto-arm",
347
333
  "scripts/lib/bridge.rb" => "scripts/lib/bridge.rb",
334
+ "scripts/lib/savepoint.rb" => "scripts/lib/savepoint.rb",
335
+ "scripts/lib/arm.rb" => "scripts/lib/arm.rb",
348
336
  "scripts/lib/lock.rb" => "scripts/lib/lock.rb",
349
337
  "scripts/plastic-lock" => "scripts/plastic-lock",
350
338
  "scripts/lib/hook_registry.rb" => "scripts/lib/hook_registry.rb",
@@ -380,13 +368,7 @@ class InstallerCore
380
368
  "scripts/validate-intent" => "scripts/validate-intent",
381
369
  "scripts/new-intent" => "scripts/new-intent",
382
370
  "scripts/end-intent" => "scripts/end-intent",
383
- "scripts/hook-create-gate" => "scripts/hook-create-gate",
384
- "scripts/hook-links-gate" => "scripts/hook-links-gate",
385
- "scripts/lib/links_gate.rb" => "scripts/lib/links_gate.rb",
386
- "scripts/lib/edit_gates.rb" => "scripts/lib/edit_gates.rb",
387
- "scripts/hook-edit-gates" => "scripts/hook-edit-gates",
388
371
  "scripts/lib/apply_patch_envelope.rb" => "scripts/lib/apply_patch_envelope.rb",
389
- "scripts/lib/codex_edit_gates.rb" => "scripts/lib/codex_edit_gates.rb",
390
372
  "scripts/lib/harness_text.rb" => "scripts/lib/harness_text.rb",
391
373
  "scripts/codex-hook" => "scripts/codex-hook",
392
374
  "scripts/spawn-preamble" => "scripts/spawn-preamble",
@@ -401,24 +383,31 @@ class InstallerCore
401
383
  "scripts/uninstall.rb" => "scripts/uninstall.rb",
402
384
  "scripts/rollback.rb" => "scripts/rollback.rb",
403
385
  "scripts/lib/outcome_guard.rb" => "scripts/lib/outcome_guard.rb",
404
- "scripts/lib/spec_header.rb" => "scripts/lib/spec_header.rb",
405
386
  "scripts/lib/scaffold_intent.rb" => "scripts/lib/scaffold_intent.rb",
387
+ "scripts/lib/backfill_intent.rb" => "scripts/lib/backfill_intent.rb",
406
388
  "scripts/scaffold-intent" => "scripts/scaffold-intent",
407
389
  "scripts/lib/verify_intent.rb" => "scripts/lib/verify_intent.rb",
408
390
  "scripts/verify-intent" => "scripts/verify-intent",
409
- "scripts/lib/start_intent.rb" => "scripts/lib/start_intent.rb",
410
- "scripts/start-intent" => "scripts/start-intent",
411
391
  "scripts/lib/exec_worktree.rb" => "scripts/lib/exec_worktree.rb",
412
392
  "scripts/exec-worktree" => "scripts/exec-worktree",
413
393
  "scripts/doctor.rb" => "scripts/doctor.rb",
414
394
  "scripts/lib/doctor_core.rb" => "scripts/lib/doctor_core.rb",
415
395
  "scripts/lib/rule_catalog.rb" => "scripts/lib/rule_catalog.rb",
416
396
  "scripts/lib/doctor_exclusions.rb" => "scripts/lib/doctor_exclusions.rb",
397
+ "scripts/lib/doctor_session_ledger.rb" => "scripts/lib/doctor_session_ledger.rb",
417
398
  "scripts/dashboard.rb" => "scripts/dashboard.rb",
418
399
  "scripts/skill-lint" => "scripts/skill-lint",
419
400
  "scripts/lib/skill_lint.rb" => "scripts/lib/skill_lint.rb",
420
401
  "scripts/feedback-report" => "scripts/feedback-report",
421
402
  "scripts/lib/feedback_report.rb" => "scripts/lib/feedback_report.rb",
403
+ "scripts/append-ledger" => "scripts/append-ledger",
404
+ "scripts/lib/session_ledger.rb" => "scripts/lib/session_ledger.rb",
405
+ "scripts/session-commit" => "scripts/session-commit",
406
+ "scripts/file-session-intent" => "scripts/file-session-intent",
407
+ "scripts/promote-session-item" => "scripts/promote-session-item",
408
+ "scripts/lib/session_backfill.rb" => "scripts/lib/session_backfill.rb",
409
+ "scripts/lib/session_close.rb" => "scripts/lib/session_close.rb",
410
+ "scripts/lib/session_git.rb" => "scripts/lib/session_git.rb",
422
411
  }
423
412
  end
424
413
 
@@ -718,7 +707,7 @@ class InstallerCore
718
707
  end
719
708
 
720
709
  # `install_skills_flat` relocates any top-level underscore-prefixed markdown
721
- # fragment (today: `_active-intent-gate.md`, `_decision-tables.md`) out of the
710
+ # fragment (today: `_decision-tables.md`) out of the
722
711
  # per-agent skills tree and into `plastic_home` directly, then lists them in
723
712
  # `installed`, so they are manifest-tracked and eligible for prune. But
724
713
  # `install_for_agent`'s prune roots are `[config[:dir], config[:home_dir]]`,
@@ -1012,7 +1001,7 @@ class InstallerCore
1012
1001
 
1013
1002
  # Copy each skills/<name>/ to <skills_root>/plastic-<name>/ (flat, namespaced by
1014
1003
  # directory name -- the only personal-skill namespacing Claude Code supports).
1015
- # Any top-level underscore-prefixed markdown fragment (e.g. `_active-intent-gate.md`,
1004
+ # Any top-level underscore-prefixed markdown fragment (e.g. `_decision-tables.md`,
1016
1005
  # `_decision-tables.md`) is a shared non-skill fragment and relocates to ~/.plastic/
1017
1006
  # instead, so every skill can read it from one shared location. `exclude` skips
1018
1007
  # named top-level skill directories entirely (intent 185: the agent-advisor skill
@@ -1273,8 +1262,8 @@ class InstallerCore
1273
1262
  plastic_hooks.each do |event, group|
1274
1263
  hooks[event] ||= []
1275
1264
 
1276
- # An event may map to a LIST of plastic groups (PreToolUse carries the
1277
- # code-gate AND the create-gate). The purge pass above already removed all
1265
+ # An event may map to a LIST of plastic groups (none does since the edit-path
1266
+ # gates left in 2.0, intent 302; the shape stays). The purge pass above already removed all
1278
1267
  # prior plastic groups, so appending each desired group fresh is idempotent
1279
1268
  # across re-runs and never collapses two matchers into one group.
1280
1269
  groups = group.is_a?(Array) ? group : [group]
@@ -49,11 +49,11 @@ module Lock
49
49
  DELEGATE_ACTIVITY_LIMIT = 20
50
50
  DELEGATE_STATUSES = %w[active finished failed].freeze
51
51
 
52
- # Lease TTL. Heartbeats fire from the write-path hooks (PostToolUse
53
- # gate-check and the lock-gate allow path), so a delivering session
54
- # refreshes constantly; 30 minutes tolerates long read-only stretches
55
- # without opening a takeover window mid-delivery. Reclaim is explicit
56
- # either way (takeover), so the TTL only bounds WHEN takeover is allowed.
52
+ # Lease TTL. Heartbeats fire from the PostToolUse record hook on every write
53
+ # inside the intent dir (intent 302), so a delivering session refreshes
54
+ # constantly; 30 minutes tolerates long read-only stretches without opening
55
+ # a takeover window mid-delivery. Reclaim is explicit either way (takeover),
56
+ # so the TTL only bounds WHEN takeover is allowed.
57
57
  TTL_SECONDS = 1800
58
58
 
59
59
  # The write guard is a mutex, not a lock in the Plastic sense: it carries no
@@ -122,7 +122,7 @@ module Lock
122
122
  Array(data["delegates"]).map(&:to_s).include?(session.to_s)
123
123
  end
124
124
 
125
- # The one question gates ask: does session hold this intent's lock?
125
+ # The one question the write path asks: does session hold this intent's lock?
126
126
  # Owner/delegate on an EXISTING lock counts even when stale (a stale lock is
127
127
  # still theirs until an explicit takeover replaces it); freshness only
128
128
  # guards AGAINST other sessions.
@@ -676,24 +676,4 @@ module Claim
676
676
  end
677
677
  end
678
678
 
679
- # Second, independent write gate at the artifact grain (intent 111 D7). Returns a
680
- # deny reason String to BLOCK, or nil to ALLOW. Composes UNDER the delivery-lock
681
- # gate: only reached after the session already holds the intent's delivery lock.
682
- # ENGAGES only when a claim file exists (dormant otherwise, so single-owner flows
683
- # and the existing suite stay green, AC7). Fails open on stale/corrupt via
684
- # fail_open?, the named contract.
685
- def claim_gate_reason(intent_dir, artifact, session:, ttl: Lock::TTL_SECONDS, now: Time.now,
686
- harness: :claude)
687
- return nil if Lock.blank?(artifact)
688
- return nil unless File.exist?(path(intent_dir, artifact)) # dormant: no claim
689
- return nil if holds_claim?(intent_dir, artifact, session: session) # you hold it
690
- return nil if fail_open?(intent_dir, artifact, ttl: ttl, now: now) # stale/corrupt: yield
691
- data = read(intent_dir, artifact)
692
- holder = data && data["owner_session"]
693
- since = data && data["acquired_at"]
694
- "artifact #{artifact} is claimed by #{holder} since #{since}; another writer holds " \
695
- "it. Back off or run #{Lock.skill_ref('plastic-doctor', harness: harness)} check the " \
696
- "lock status. If you are a distinct delegate, the owner must register you: " \
697
- "plastic-lock delegate --intent-dir #{intent_dir} --session <your-session-id>"
698
- end
699
679
  end
@@ -1,14 +1,16 @@
1
1
  # encoding: UTF-8
2
2
  # frozen_string_literal: true
3
3
 
4
- require_relative "bridge"
4
+ require_relative "savepoint"
5
5
  require_relative "intent_validator"
6
6
 
7
7
  # OutcomeGuard - the single definition of "is outcome.md real and disposition-matched for
8
8
  # THIS close" (intent 222). Extracted verbatim from scripts/end-intent's own
9
- # outcome_guard_reason (intent 161/188) so end-intent's existing exit-2 path and doctor's new
10
- # per-intent check (intent_lifecycle_artifacts) can never independently drift on what counts
11
- # as a valid outcome.md. Pure, read-only: authors nothing regardless of outcome.
9
+ # outcome_guard_reason (intent 161/188) so its two callers, doctor's per-intent check
10
+ # (intent_lifecycle_artifacts) and end-intent's post-backfill self-check, can never drift on
11
+ # what counts as a valid outcome.md. Since intent 308 nothing blocks on it: end-intent
12
+ # backfills a missing or placeholder outcome.md first, prints a non-nil reason, and proceeds.
13
+ # Pure, read-only: authors nothing regardless of outcome.
12
14
  module OutcomeGuard
13
15
  module_function
14
16
 
@@ -23,7 +25,7 @@ module OutcomeGuard
23
25
 
24
26
  content = File.read(path)
25
27
  first_line = content.each_line.first.to_s.chomp
26
- if first_line == Bridge::PLACEHOLDER_SENTINEL
28
+ if first_line == Savepoint::PLACEHOLDER_SENTINEL
27
29
  return "outcome.md is still the scaffold placeholder (first line is the sentinel)"
28
30
  end
29
31
 
@@ -24,6 +24,15 @@ require "yaml"
24
24
  module ProjectValidator
25
25
  module_function
26
26
 
27
+ # The two enumerated flow: knobs (intent 300, spec D2). Kept as a plain
28
+ # literal here rather than a require on SessionGit: this validator only
29
+ # reports a bad value in `errors` (project.yml is still "accepted", spec
30
+ # D2's matrix row), it never blocks spawn completeness, since a bad value
31
+ # degrades gracefully at commit time (SessionGit falls back with a Note)
32
+ # rather than blocking the spawn.
33
+ FLOW_MODES = %w[direct pull_request].freeze
34
+ FLOW_WORKSPACES = %w[checkout worktree].freeze
35
+
27
36
  def validate(slug, plastic_home: File.join(Dir.home, ".plastic"))
28
37
  missing = []
29
38
  errors = []
@@ -53,7 +62,9 @@ module ProjectValidator
53
62
  rescue StandardError
54
63
  nil
55
64
  end
56
- unless parsed.is_a?(Hash)
65
+ if parsed.is_a?(Hash)
66
+ validate_flow_block(parsed, errors)
67
+ else
57
68
  missing << "project.yml (valid YAML)"
58
69
  errors << "project.yml exists at #{project_yml_path} but does not parse as YAML"
59
70
  end
@@ -88,6 +99,24 @@ module ProjectValidator
88
99
  { ok: missing.empty?, missing: missing, errors: errors }
89
100
  end
90
101
 
102
+ # Non-blocking: an unknown `mode` or `workspace` value in project.yml's
103
+ # `flow:` block, if present, is reported in `errors` but never added to
104
+ # `missing`, so it never flips `ok`.
105
+ def validate_flow_block(parsed, errors)
106
+ flow = parsed["flow"]
107
+ return unless flow.is_a?(Hash)
108
+
109
+ validate_flow_knob(flow, "mode", FLOW_MODES, errors)
110
+ validate_flow_knob(flow, "workspace", FLOW_WORKSPACES, errors)
111
+ end
112
+
113
+ def validate_flow_knob(flow, key, allowed, errors)
114
+ value = flow[key]
115
+ return if value.nil? || allowed.include?(value.to_s)
116
+
117
+ errors << "project.yml flow.#{key} is #{value.inspect}, must be one of #{allowed.join(", ")}"
118
+ end
119
+
91
120
  # Invariant 1: registered in projects.yml with a 'path'. Returns the
92
121
  # project's entry Hash, or nil when unregistered or the entry has no path.
93
122
  def registration_for(slug, plastic_home)
@@ -20,10 +20,10 @@ class FileOrderRanker
20
20
  end
21
21
  end
22
22
 
23
- # RoadmapQueue - the one deterministic reader the auto loop and plastic-roadmap-continuing
23
+ # RoadmapQueue - the one deterministic reader the auto loop and plastic-intent-continuing
24
24
  # both call (intent 148). Constructor-DI, hermetic: clock and paths injected, no eval, no ENV
25
25
  # or global config seam. It does two things: liveness-ranks a tier's roadmaps/*.md files
26
- # (porting plastic-roadmap-continuing's read-time algorithm), and, within the winning
26
+ # (porting plastic-intent-continuing's read-time algorithm), and, within the winning
27
27
  # roadmap, selects the frontier wave plus its dispatchable set (D-b), value-ordered by the
28
28
  # injected ranker (default FileOrderRanker, the intent-173 swap seam). Every frontier token is
29
29
  # reconciled against INDEX.md first, INDEX wins. Reads through the 134 ledger via the public
@@ -181,7 +181,7 @@ class RoadmapQueue
181
181
  File.join(File.dirname(@roadmaps_dir), "INDEX.md")
182
182
  end
183
183
 
184
- # --- liveness ranking (ports plastic-roadmap-continuing's read-time algorithm) -
184
+ # --- liveness ranking (ports plastic-intent-continuing's read-time algorithm) -
185
185
 
186
186
  def rank_candidates(parsed_list)
187
187
  parsed_list.map do |c|
@@ -102,7 +102,7 @@ module RoadmapSavepoint
102
102
  # entry has a `merged` line. Every timestamp comes from an on-disk source (the Log, or INDEX
103
103
  # `## Completed`); an entry with no recoverable timestamp is not emitted (D4, never invented).
104
104
  # Overwrites the ledger (the one operation allowed to rewrite it, matching
105
- # `Bridge.rebuild_savepoint`). Returns the number of lines written.
105
+ # `Savepoint.rebuild_savepoint`). Returns the number of lines written.
106
106
  def rebuild(roadmap_path)
107
107
  text = File.read(roadmap_path)
108
108
  log_lines = classify_log(section_body(text, "Log"))
@@ -5,9 +5,9 @@
5
5
  # different axes so there is exactly one place to look up or register a rule name:
6
6
  #
7
7
  # EXCLUDABLE_CHECKS - a doctor check `name` a per-store doctor-exclusions file may name. A
8
- # check name says WHICH DIAGNOSTIC FIRED. v1 carries exactly one key, savepoint_operational
9
- # (see spec D3): most doctor checks have no exclusion mechanism at all, and this is the only
10
- # one the owner asked to make skippable.
8
+ # check name says WHICH DIAGNOSTIC FIRED. Two keys: savepoint_operational (274, spec D3) and
9
+ # backfilled_complete (308). Most doctor checks have no exclusion mechanism at all; these two
10
+ # are the ones whose gap can be a known, accepted fact on an old or migrated intent.
11
11
  #
12
12
  # REVISION_RULES - the `[rule: <tag>]` vocabulary every revisions.md entry must carry
13
13
  # (scripts/lib/revisions_writer.rb). A tag says WHY an intent's files were structurally
@@ -33,6 +33,9 @@ module RuleCatalog
33
33
  "delivered|abandoned echo, on a terminal intent. Usually " \
34
34
  "repairable via maintenance-run --tool rebuild-savepoint; " \
35
35
  "excludable for the gaps 219 D6 forbids ever repairing.",
36
+ "backfilled_complete" => "spec.md or plan.md missing or still the placeholder, or no real " \
37
+ "action file, on a terminal intent. Repairable via scaffold-intent " \
38
+ "backfill; excludable for an old intent whose record was never kept.",
36
39
  }.freeze
37
40
 
38
41
  # Measured from live store data across all eight stores, 2026-08-24 (spec D1).