@zalom/plastic 1.14.1 → 2.0.0-alpha.10
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/bin/lib/context_budget.rb +453 -0
- package/bin/plastic-bench +78 -0
- package/config_asks.yml +38 -0
- package/hooks/capture +4 -0
- package/hooks/close +5 -0
- package/hooks/hooks.json +14 -42
- package/hooks/message-display +81 -0
- package/hooks/record +4 -0
- package/hooks/savepoint +5 -5
- package/package.json +3 -2
- package/scripts/agent-report +15 -10
- package/scripts/append-ledger +208 -0
- package/scripts/codex-hook +78 -90
- package/scripts/dashboard.rb +43 -14
- package/scripts/day-summary +53 -0
- package/scripts/doctor.rb +269 -43
- package/scripts/end-intent +170 -76
- package/scripts/exec-worktree +11 -20
- package/scripts/file-session-intent +61 -0
- package/scripts/hook-capture +255 -0
- package/scripts/hook-close +36 -0
- package/scripts/hook-message-display +74 -0
- package/scripts/hook-record +157 -0
- package/scripts/hook-savepoint +45 -0
- package/scripts/hook-session-start +128 -12
- package/scripts/install.rb +8 -6
- package/scripts/intent-screen +77 -0
- package/scripts/lib/agent_models.rb +1 -7
- package/scripts/lib/arm.rb +308 -0
- package/scripts/lib/backfill_intent.rb +316 -0
- package/scripts/lib/bridge.rb +24 -1597
- package/scripts/lib/compact_instructions.rb +56 -0
- package/scripts/lib/day_summary.rb +211 -0
- package/scripts/lib/doctor_core.rb +66 -88
- package/scripts/lib/doctor_session_ledger.rb +158 -0
- package/scripts/lib/exec_worktree.rb +14 -60
- package/scripts/lib/handoff.rb +184 -0
- package/scripts/lib/harness_text.rb +5 -4
- package/scripts/lib/hook_registry.rb +57 -106
- package/scripts/lib/insights.rb +3 -4
- package/scripts/lib/installer_core.rb +136 -41
- package/scripts/lib/intent_screen.rb +309 -0
- package/scripts/lib/intent_screen_ansi.rb +262 -0
- package/scripts/lib/lock.rb +6 -26
- package/scripts/lib/message_display.rb +290 -0
- package/scripts/lib/outcome_guard.rb +7 -5
- package/scripts/lib/project_validator.rb +30 -1
- package/scripts/lib/report_screen.rb +648 -0
- 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 +334 -0
- package/scripts/lib/scaffold_intent.rb +13 -179
- package/scripts/lib/screen_paint.rb +276 -0
- package/scripts/lib/session_backfill.rb +184 -0
- package/scripts/lib/session_close.rb +104 -0
- package/scripts/lib/session_git.rb +500 -0
- package/scripts/lib/session_ledger.rb +621 -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 +101 -41
- package/scripts/promote-session-item +150 -0
- package/scripts/read-config +3 -0
- package/scripts/rebuild-graph +1 -1
- package/scripts/report-screen +120 -0
- package/scripts/roadmap-next +2 -2
- package/scripts/rollback.rb +48 -3
- package/scripts/savepoint-note +67 -0
- package/scripts/scaffold-intent +37 -63
- package/scripts/session-commit +116 -0
- package/scripts/spawn-preamble +14 -7
- package/scripts/update.rb +7 -3
- package/scripts/write-handoff +60 -0
- package/skills/agent-advisor/SKILL.md +3 -3
- package/skills/agent-advisor/references/advisor-protocol.md +20 -20
- package/skills/auto/SKILL.md +223 -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 +62 -56
- 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 +62 -58
- 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 +142 -91
- package/skills/intent-continuing/references/boarding-matrix.md +34 -0
- package/skills/intent-continuing/references/context-management.md +3 -3
- package/skills/intent-creating/SKILL.md +2 -2
- package/skills/intent-creating/references/lifecycle.md +1 -1
- package/skills/intent-ending/SKILL.md +34 -37
- package/skills/intent-ending/evals/evals.json +4 -4
- package/skills/intent-executing/SKILL.md +15 -46
- 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/config.yml +5 -0
- package/templates/intent-screen.md +17 -0
- package/templates/outcome.md +14 -1
- package/templates/project.yml +7 -0
- package/templates/report-state.md +11 -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
|
@@ -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
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Handoff (intent 311): one session's hand-off, a pure rendering of its share
|
|
5
|
+
# of a day ledger (checklist.md and savepoint.md), written into the day
|
|
6
|
+
# directory as handoff--<session>.md at every tick, at PreCompact, and at
|
|
7
|
+
# close. Derived and regenerable: every write renders in full, so a lost or
|
|
8
|
+
# stale copy costs nothing. No environment reads; every path is injected.
|
|
9
|
+
|
|
10
|
+
require "fileutils"
|
|
11
|
+
require_relative "session_ledger"
|
|
12
|
+
|
|
13
|
+
module Handoff
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
TRIGGERS = %w[tick precompact close].freeze
|
|
17
|
+
BUDGET = 6144
|
|
18
|
+
OPEN_CAP = 20
|
|
19
|
+
DONE_CAP = 10
|
|
20
|
+
RECENT_CAP = 10
|
|
21
|
+
OTHERS_CAP = 10
|
|
22
|
+
# A ledger summary may run to 200 characters; a hand-off line shows the
|
|
23
|
+
# first 80, so the caps above are reachable inside the byte budget.
|
|
24
|
+
SUMMARY_MAX = 80
|
|
25
|
+
OPEN_STATES = %i[open pending].freeze
|
|
26
|
+
# Trimmed first when the budget is exceeded; Open is the last to shrink.
|
|
27
|
+
TRIM_ORDER = %i[others recent done open].freeze
|
|
28
|
+
RESUME = "Say continue; the day summary at boot and this file carry the state."
|
|
29
|
+
|
|
30
|
+
SAVEPOINT_TAIL_RE = /\A\[([^\]]*)\] \[([^\]]*)\] (.*)\z/m
|
|
31
|
+
private_constant :SAVEPOINT_TAIL_RE
|
|
32
|
+
|
|
33
|
+
def path_for(store, day, session)
|
|
34
|
+
File.join(SessionLedger.day_dir(store, day), "handoff--#{session}.md")
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# The day this session's hand-off belongs to: the pointer's day id when
|
|
38
|
+
# the pointer holds one, else today (no pointer, or a pointer naming an
|
|
39
|
+
# intent), the same fallback SessionClose uses for the drop at close.
|
|
40
|
+
def day_for(store, session, today:)
|
|
41
|
+
path = SessionLedger.pointer_path(store, session)
|
|
42
|
+
return today unless File.exist?(path)
|
|
43
|
+
|
|
44
|
+
value = File.read(path).strip
|
|
45
|
+
SessionLedger.valid_day_id?(value) ? value : today
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def clip(summary)
|
|
49
|
+
text = summary.to_s
|
|
50
|
+
text.length > SUMMARY_MAX ? "#{text[0, SUMMARY_MAX]}..." : text
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# --- readers ---------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
def read_items(store, day)
|
|
56
|
+
SessionLedger.read_locked(SessionLedger.checklist_path(store, day))
|
|
57
|
+
.each_line.filter_map { |l| SessionLedger.parse_checklist_line(l) }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Parsed savepoint lines, file order: {time:, event:, session:, project:,
|
|
61
|
+
# summary:}. A line that does not follow the ledger grammar is skipped.
|
|
62
|
+
def read_savepoint(store, day)
|
|
63
|
+
SessionLedger.read_locked(SessionLedger.savepoint_path(store, day)).each_line.filter_map do |raw|
|
|
64
|
+
line = raw.chomp.scrub
|
|
65
|
+
next if line.empty?
|
|
66
|
+
|
|
67
|
+
time, event, rest = line.split(/\s{2,}/, 3)
|
|
68
|
+
next unless time && event && rest
|
|
69
|
+
|
|
70
|
+
match = SAVEPOINT_TAIL_RE.match(rest)
|
|
71
|
+
next unless match
|
|
72
|
+
|
|
73
|
+
{ time: time, event: event, session: match[1], project: match[2], summary: match[3] }
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# --- rendering, pure -----------------------------------------------------------
|
|
78
|
+
|
|
79
|
+
def render(store:, day:, session:, trigger:, now: Time.now)
|
|
80
|
+
raise ArgumentError, "unknown trigger: #{trigger.inspect}" unless TRIGGERS.include?(trigger)
|
|
81
|
+
|
|
82
|
+
items = read_items(store, day)
|
|
83
|
+
mine = items.select { |i| i[:session] == session }
|
|
84
|
+
lists = {
|
|
85
|
+
open: mine.select { |i| OPEN_STATES.include?(i[:state]) }.map { |i| item_line(i) },
|
|
86
|
+
done: mine.select { |i| i[:state] == :done }.map { |i| item_line(i) },
|
|
87
|
+
recent: read_savepoint(store, day).select { |e| e[:session] == session }.map { |e| recent_line(e) },
|
|
88
|
+
others: others_lines(items, session),
|
|
89
|
+
}
|
|
90
|
+
hidden = Hash.new(0)
|
|
91
|
+
cap!(lists, hidden, :open, OPEN_CAP)
|
|
92
|
+
cap!(lists, hidden, :done, DONE_CAP)
|
|
93
|
+
cap!(lists, hidden, :recent, RECENT_CAP)
|
|
94
|
+
if lists[:others].size > OTHERS_CAP
|
|
95
|
+
hidden[:others] += lists[:others].size - OTHERS_CAP
|
|
96
|
+
lists[:others] = lists[:others].first(OTHERS_CAP)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
header = [
|
|
100
|
+
"# Hand-off: session #{session}, #{day}",
|
|
101
|
+
"",
|
|
102
|
+
"Written #{now.utc.strftime('%Y-%m-%dT%H:%M:%SZ')} at #{trigger}",
|
|
103
|
+
"",
|
|
104
|
+
]
|
|
105
|
+
loop do
|
|
106
|
+
text = compose(header, lists, hidden)
|
|
107
|
+
return text if text.bytesize <= BUDGET
|
|
108
|
+
|
|
109
|
+
key = TRIM_ORDER.find { |k| !lists[k].empty? }
|
|
110
|
+
return text unless key
|
|
111
|
+
|
|
112
|
+
# Open, Done, and Recent keep their newest entries; Others has no order.
|
|
113
|
+
key == :others ? lists[key].pop : lists[key].shift
|
|
114
|
+
hidden[key] += 1
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Keeps the newest `cap` lines (the file is chronological) and counts the rest.
|
|
119
|
+
def cap!(lists, hidden, key, cap)
|
|
120
|
+
return unless lists[key].size > cap
|
|
121
|
+
|
|
122
|
+
hidden[key] += lists[key].size - cap
|
|
123
|
+
lists[key] = lists[key].last(cap)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def compose(header, lists, hidden)
|
|
127
|
+
sections = [
|
|
128
|
+
section("Open", lists[:open], hidden[:open]),
|
|
129
|
+
section("Done", lists[:done], hidden[:done]),
|
|
130
|
+
section("Recent", lists[:recent], hidden[:recent]),
|
|
131
|
+
section("Others today", lists[:others], hidden[:others]),
|
|
132
|
+
"## Resume\n#{RESUME}\n",
|
|
133
|
+
]
|
|
134
|
+
(header + sections.compact).join("\n")
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def section(title, lines, hidden)
|
|
138
|
+
return nil if lines.empty? && hidden.zero?
|
|
139
|
+
|
|
140
|
+
body = lines.dup
|
|
141
|
+
body << "(+#{hidden} more)" if hidden.positive?
|
|
142
|
+
"## #{title}\n#{body.join("\n")}\n"
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def item_line(item)
|
|
146
|
+
"- [#{item[:project]}] #{clip(item[:summary])}"
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def recent_line(event)
|
|
150
|
+
"- #{event[:time][11, 5]}Z #{event[:event]} #{clip(event[:summary])}"
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def others_lines(items, session)
|
|
154
|
+
items.reject { |i| i[:session] == session }
|
|
155
|
+
.group_by { |i| i[:session] }
|
|
156
|
+
.sort
|
|
157
|
+
.map do |sid, theirs|
|
|
158
|
+
open = theirs.count { |i| OPEN_STATES.include?(i[:state]) }
|
|
159
|
+
done = theirs.count { |i| i[:state] == :done }
|
|
160
|
+
"- #{sid}: #{open} open, #{done} done"
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# --- writing -------------------------------------------------------------------
|
|
165
|
+
|
|
166
|
+
# Opens the day first (a tick after midnight never fails), renders, and
|
|
167
|
+
# writes through a per-process temp file and rename, so a crash leaves no
|
|
168
|
+
# partial hand-off and two writers for one session (a tick overlapping a
|
|
169
|
+
# PreCompact) never share a temp name. Returns the path. With
|
|
170
|
+
# `templates: nil` the day is not scaffolded, only its directory ensured.
|
|
171
|
+
def write(store:, day:, session:, trigger:, templates:, now: Time.now)
|
|
172
|
+
if templates
|
|
173
|
+
SessionLedger.open_day(store: store, day: day, templates: templates, author: session)
|
|
174
|
+
else
|
|
175
|
+
FileUtils.mkdir_p(SessionLedger.day_dir(store, day))
|
|
176
|
+
end
|
|
177
|
+
text = render(store: store, day: day, session: session, trigger: trigger, now: now)
|
|
178
|
+
target = path_for(store, day, session)
|
|
179
|
+
tmp = File.join(File.dirname(target), ".handoff-#{session}-#{Process.pid}-#{Thread.current.object_id}.tmp")
|
|
180
|
+
File.write(tmp, text)
|
|
181
|
+
File.rename(tmp, target)
|
|
182
|
+
target
|
|
183
|
+
end
|
|
184
|
+
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?
|
|
@@ -10,9 +10,8 @@
|
|
|
10
10
|
module HookRegistry
|
|
11
11
|
module_function
|
|
12
12
|
|
|
13
|
-
# MCP tools that MUTATE files.
|
|
14
|
-
#
|
|
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,68 @@ module HookRegistry
|
|
|
74
39
|
{ "name" => "savepoint", "status" => "Saving Plastic intent state..." },
|
|
75
40
|
] },
|
|
76
41
|
],
|
|
77
|
-
"
|
|
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" => "
|
|
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
|
-
"
|
|
90
|
-
{ "matcher" => "
|
|
91
|
-
{ "name" => "
|
|
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" => "
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
54
|
+
{ "name" => "capture", "status" => "Capturing prompt into the session ledger..." },
|
|
55
|
+
] },
|
|
56
|
+
],
|
|
57
|
+
# This entry belongs to the Claude adapter half of Plastic's
|
|
58
|
+
# harness-agnostic-core / Claude-adapter split (intent 316a1, D3
|
|
59
|
+
# supersedes 316a's D6): MessageDisplay is not one of
|
|
60
|
+
# CODEX_LIVE_STATE_EVENTS, so codex_hooks_json (below) never picks it
|
|
61
|
+
# up; codex_hook_names stays exactly what it was (pinned by
|
|
62
|
+
# test/hook_registry_test.rb:82 and :110-111). Fires on every streamed
|
|
63
|
+
# chunk of every assistant message (D11); the launcher (hooks/message-
|
|
64
|
+
# display) decides with shell builtins and forks nothing on the common
|
|
65
|
+
# case, execing Ruby only for a candidate message.
|
|
66
|
+
"MessageDisplay" => [
|
|
67
|
+
{ "matcher" => "", "hooks" => [
|
|
68
|
+
{ "name" => "message-display", "status" => "" },
|
|
100
69
|
] },
|
|
101
70
|
],
|
|
102
71
|
}
|
|
103
72
|
end
|
|
104
73
|
|
|
105
74
|
# Codex registration (~/.codex/hooks.json, intent 102). Derived from `events`:
|
|
106
|
-
# the
|
|
107
|
-
#
|
|
108
|
-
#
|
|
109
|
-
#
|
|
110
|
-
#
|
|
111
|
-
# [
|
|
112
|
-
#
|
|
113
|
-
#
|
|
114
|
-
#
|
|
115
|
-
|
|
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
|
|
75
|
+
# the PostToolUse record hook collapses from Claude's multi-tool matcher onto
|
|
76
|
+
# Codex's single apply_patch tool (181 F4: apply_patch is Codex's sole
|
|
77
|
+
# file-mutation tool; tool_name always reports apply_patch), and the live-state
|
|
78
|
+
# events project through whole. Since intent 302 there is no PreToolUse group at
|
|
79
|
+
# all: the edit-path gates are gone on both harnesses. Command invokes the
|
|
80
|
+
# codex-hook dispatcher with the hook name. Guide-settled shape [guide Part 3]:
|
|
81
|
+
# top-level {"hooks":{<Event>: [{"matcher","hooks":[{"type":"command","command",
|
|
82
|
+
# "statusMessage"}]}]}}, identical to Claude's shape, string command. Single
|
|
83
|
+
# source of truth (108 D7): any drift from `events` is a bug, pinned by test.
|
|
84
|
+
CODEX_POST_HOOKS = %w[record].freeze
|
|
123
85
|
|
|
124
|
-
#
|
|
125
|
-
#
|
|
126
|
-
#
|
|
127
|
-
#
|
|
128
|
-
#
|
|
129
|
-
#
|
|
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.
|
|
86
|
+
# Live-state events registered WHOLE (intent 199): Codex's SessionStart/
|
|
87
|
+
# UserPromptSubmit/PreCompact already match Claude's shape exactly, one matcher
|
|
88
|
+
# group each ("", no tool to collapse onto), so every hook `events` lists under
|
|
89
|
+
# these three events projects straight through with no allowlist to keep in
|
|
90
|
+
# sync. A hook added to any of them on the Claude side registers for Codex
|
|
91
|
+
# automatically.
|
|
140
92
|
CODEX_LIVE_STATE_EVENTS = %w[SessionStart UserPromptSubmit PreCompact].freeze
|
|
141
93
|
|
|
94
|
+
# SessionEnd on Codex (intent 309): projected from its own constant, never as a fourth
|
|
95
|
+
# live-state event, because the dispatch differs. Codex kills a SessionEnd hook after
|
|
96
|
+
# 3 seconds, so scripts/codex-hook hands `close` to its launcher detached and returns at
|
|
97
|
+
# once instead of the synchronous relay the live-state hooks get. Codex ships both Stop
|
|
98
|
+
# and SessionEnd (codex-rs/hooks at rust-v0.149.1); the design uses SessionEnd on both
|
|
99
|
+
# harnesses. Like STATE_HOOKS, the dispatcher's list is hand-kept and cross-checked by
|
|
100
|
+
# test/hook_registry_test.rb.
|
|
101
|
+
CODEX_SESSION_END_HOOKS = %w[close].freeze
|
|
102
|
+
|
|
142
103
|
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
104
|
status_by_name = events.values.flatten.flat_map { |g| g["hooks"] }
|
|
147
105
|
.each_with_object({}) { |h, m| m[h["name"]] = h["status"] }
|
|
148
106
|
cmd = ->(name) {
|
|
@@ -150,27 +108,19 @@ module HookRegistry
|
|
|
150
108
|
"command" => "\"#{dispatcher_path}\" #{name}",
|
|
151
109
|
"statusMessage" => status_by_name[name].to_s }
|
|
152
110
|
}
|
|
153
|
-
#
|
|
154
|
-
|
|
155
|
-
|
|
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) }
|
|
111
|
+
# Validate the Codex name against the single source of truth, `events`.
|
|
112
|
+
post_order = events["PostToolUse"].flat_map { |g| g["hooks"].map { |h| h["name"] } }
|
|
113
|
+
post = (CODEX_POST_HOOKS & post_order).map { |n| cmd.call(n) }
|
|
162
114
|
|
|
163
115
|
result = {
|
|
164
|
-
"PreToolUse" => [
|
|
165
|
-
{ "matcher" => "apply_patch", "hooks" => pre },
|
|
166
|
-
{ "matcher" => "Bash", "hooks" => bash },
|
|
167
|
-
],
|
|
168
116
|
"PostToolUse" => [{ "matcher" => "apply_patch", "hooks" => post }],
|
|
169
117
|
}
|
|
170
118
|
CODEX_LIVE_STATE_EVENTS.each do |event|
|
|
171
119
|
names = events[event].flat_map { |g| g["hooks"].map { |h| h["name"] } }
|
|
172
120
|
result[event] = [{ "matcher" => "", "hooks" => names.map { |n| cmd.call(n) } }]
|
|
173
121
|
end
|
|
122
|
+
end_order = events["SessionEnd"].flat_map { |g| g["hooks"].map { |h| h["name"] } }
|
|
123
|
+
result["SessionEnd"] = [{ "matcher" => "", "hooks" => (CODEX_SESSION_END_HOOKS & end_order).map { |n| cmd.call(n) } }]
|
|
174
124
|
result
|
|
175
125
|
end
|
|
176
126
|
|
|
@@ -179,8 +129,7 @@ module HookRegistry
|
|
|
179
129
|
# installed as ~/.claude/hooks/plastic-<name>. The single derivation doctor's
|
|
180
130
|
# hooks_exist/hooks_executable/hooks_no_orphans checks read from, so a
|
|
181
131
|
# hand-kept list of launchers can never drift out of step with `events`
|
|
182
|
-
# again (the gap that let 8 of 15 launchers
|
|
183
|
-
# unchecked).
|
|
132
|
+
# again (the gap that once let 8 of 15 launchers go unchecked).
|
|
184
133
|
def claude_launcher_names
|
|
185
134
|
events.values.flatten.flat_map { |g| g["hooks"].map { |h| h["name"] } }
|
|
186
135
|
.uniq.sort.map { |name| "plastic-#{name}" }
|
|
@@ -204,8 +153,10 @@ module HookRegistry
|
|
|
204
153
|
# hooks_exist demands be present on disk, so a retired name there makes a correct
|
|
205
154
|
# install report missing launchers.
|
|
206
155
|
RETIRED_HOOK_NAMES = %w[
|
|
207
|
-
code-gate create-gate links-gate lock-gate savepoint-pre
|
|
156
|
+
edit-gates bash-gate code-gate create-gate links-gate lock-gate savepoint-pre
|
|
208
157
|
qmd-search retrieval-gate model-instructions opus-manual
|
|
158
|
+
continue future-intent-check auto-arm gate-check
|
|
159
|
+
power-tools
|
|
209
160
|
].freeze
|
|
210
161
|
|
|
211
162
|
RETIRED_CLAUDE_LAUNCHERS = RETIRED_HOOK_NAMES.map { |n| "plastic-#{n}" }.freeze
|
|
@@ -226,7 +177,7 @@ module HookRegistry
|
|
|
226
177
|
live = CODEX_LIVE_STATE_EVENTS.flat_map do |event|
|
|
227
178
|
events[event].flat_map { |g| g["hooks"].map { |h| h["name"] } }
|
|
228
179
|
end
|
|
229
|
-
(
|
|
180
|
+
(CODEX_POST_HOOKS + live + CODEX_SESSION_END_HOOKS).uniq.sort
|
|
230
181
|
end
|
|
231
182
|
|
|
232
183
|
def codex_purgeable_hook_names
|
package/scripts/lib/insights.rb
CHANGED
|
@@ -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 `
|
|
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 "
|
|
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 =
|
|
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
|