@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
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# encoding: UTF-8
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
# append-ledger (intent 297) - the only writer of a session day ledger's
|
|
6
|
+
# shared files. Every verb either appends a line to checklist.md or
|
|
7
|
+
# savepoint.md, or flips one checklist line's state marker byte, always
|
|
8
|
+
# under an exclusive lock on the target file itself (spec D6, D7, D8). Every
|
|
9
|
+
# invocation scaffolds the day itself through SessionLedger.open_day (cheap
|
|
10
|
+
# and idempotent), so a capture that crosses midnight never fails and never
|
|
11
|
+
# needs a second process (spec D12).
|
|
12
|
+
#
|
|
13
|
+
# Usage:
|
|
14
|
+
# append-ledger pending <summary> [common flags]
|
|
15
|
+
# append-ledger item <summary> [common flags]
|
|
16
|
+
# append-ledger promote [--match <substring>] [--savepoint] [common flags]
|
|
17
|
+
# append-ledger tick [--match <substring>] [--savepoint] [common flags]
|
|
18
|
+
# append-ledger savepoint --event Item|Done|Note <summary> [common flags]
|
|
19
|
+
#
|
|
20
|
+
# Common flags:
|
|
21
|
+
# --store <dir> defaults to $PLASTIC_HOME/store ($PLASTIC_HOME defaults to ~/.plastic)
|
|
22
|
+
# --day <YYYYMMDD> defaults to SessionLedger.day_id (today, local wall clock)
|
|
23
|
+
# --session <id> defaults to $CLAUDE_CODE_SESSION_ID, then "local"
|
|
24
|
+
# --project <slug> defaults to the project_slug derived from --cwd
|
|
25
|
+
# --cwd <dir> defaults to Dir.pwd
|
|
26
|
+
# --templates <dir> defaults to the templates/ dir shipped next to this script
|
|
27
|
+
#
|
|
28
|
+
# Exit codes:
|
|
29
|
+
# 0 - done
|
|
30
|
+
# 2 - usage error, or promote/tick found nothing to address (no write happens)
|
|
31
|
+
# 3 - promote/tick could not take an exclusive lock (filesystem without flock support)
|
|
32
|
+
|
|
33
|
+
require_relative "lib/session_ledger"
|
|
34
|
+
|
|
35
|
+
VERBS = %w[pending item promote tick savepoint].freeze
|
|
36
|
+
|
|
37
|
+
def usage_abort(message)
|
|
38
|
+
warn "append-ledger: #{message}"
|
|
39
|
+
exit 2
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# The value token that follows a flag at argv[i]. Aborts with exit 2 rather
|
|
43
|
+
# than silently returning nil when the flag is the last token (a trailing
|
|
44
|
+
# `--store` with nothing after it must not fall back to a default as if the
|
|
45
|
+
# flag had never been given).
|
|
46
|
+
def next_value!(argv, i, flag)
|
|
47
|
+
usage_abort("#{flag} requires a value") if i + 1 >= argv.length
|
|
48
|
+
argv[i + 1]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def parse_args(argv)
|
|
52
|
+
opts = {
|
|
53
|
+
match: nil, savepoint: false, event: nil,
|
|
54
|
+
store: nil, day: nil, session: nil, project: nil, cwd: nil, templates: nil,
|
|
55
|
+
}
|
|
56
|
+
positional = []
|
|
57
|
+
i = 0
|
|
58
|
+
while i < argv.length
|
|
59
|
+
arg = argv[i]
|
|
60
|
+
case arg
|
|
61
|
+
when "--match" then opts[:match] = next_value!(argv, i, arg); i += 1
|
|
62
|
+
when "--event" then opts[:event] = next_value!(argv, i, arg); i += 1
|
|
63
|
+
when "--store" then opts[:store] = next_value!(argv, i, arg); i += 1
|
|
64
|
+
when "--day" then opts[:day] = next_value!(argv, i, arg); i += 1
|
|
65
|
+
when "--session" then opts[:session] = next_value!(argv, i, arg); i += 1
|
|
66
|
+
when "--project" then opts[:project] = next_value!(argv, i, arg); i += 1
|
|
67
|
+
when "--cwd" then opts[:cwd] = next_value!(argv, i, arg); i += 1
|
|
68
|
+
when "--templates" then opts[:templates] = next_value!(argv, i, arg); i += 1
|
|
69
|
+
when "--savepoint" then opts[:savepoint] = true
|
|
70
|
+
else
|
|
71
|
+
usage_abort("unknown flag #{arg.inspect}") if arg.start_with?("--")
|
|
72
|
+
|
|
73
|
+
positional << arg
|
|
74
|
+
end
|
|
75
|
+
i += 1
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
opts[:verb] = positional.shift
|
|
79
|
+
opts[:summary] = positional.shift
|
|
80
|
+
opts
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def expand(path)
|
|
84
|
+
File.expand_path(path.to_s.sub(/\A~/, Dir.home))
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Sibling of this script's dir. Works in-repo (<repo>/scripts/append-ledger
|
|
88
|
+
# -> <repo>/templates) and installed (~/.plastic/scripts/append-ledger ->
|
|
89
|
+
# ~/.plastic/templates when present).
|
|
90
|
+
def default_templates_dir
|
|
91
|
+
File.expand_path("../templates", __dir__)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def sanitized_summary(raw)
|
|
95
|
+
usage_abort("a summary is required") if raw.nil?
|
|
96
|
+
|
|
97
|
+
clean = SessionLedger.sanitize_summary(raw)
|
|
98
|
+
usage_abort("summary is empty after sanitizing") if clean.empty?
|
|
99
|
+
clean
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def append_checklist_line(state, store, day, session, project, summary)
|
|
103
|
+
clean = sanitized_summary(summary)
|
|
104
|
+
line = SessionLedger.checklist_line(state, session, project, clean)
|
|
105
|
+
path = SessionLedger.checklist_path(store, day)
|
|
106
|
+
SessionLedger.append_line(path, line, header: SessionLedger.checklist_header(day))
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# set_state identifies the target line AND flips it inside one LOCK_EX hold,
|
|
110
|
+
# returning the flipped line's own summary (or nil when nothing matched).
|
|
111
|
+
# There is deliberately no separate identify-then-flip step here: a prior
|
|
112
|
+
# version located the target under its own released lock, then set_state
|
|
113
|
+
# re-located it under a fresh lock, and the two scans could disagree under
|
|
114
|
+
# concurrency, naming the wrong line in the --savepoint entry below. Using
|
|
115
|
+
# set_state's return value is what keeps identify-and-flip atomic.
|
|
116
|
+
def run_transition(from, to, event, store, day, session, project, opts)
|
|
117
|
+
path = SessionLedger.checklist_path(store, day)
|
|
118
|
+
|
|
119
|
+
target_summary =
|
|
120
|
+
begin
|
|
121
|
+
SessionLedger.set_state(path, from: from, to: to, session: session, match: opts[:match])
|
|
122
|
+
rescue SessionLedger::LockUnavailableError => e
|
|
123
|
+
warn "append-ledger: #{e.message}"
|
|
124
|
+
exit 3
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
if target_summary.nil?
|
|
128
|
+
warn "append-ledger: nothing to address"
|
|
129
|
+
exit 2
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
return unless opts[:savepoint]
|
|
133
|
+
|
|
134
|
+
line = SessionLedger.savepoint_line(event, session, project, target_summary, now: Time.now)
|
|
135
|
+
SessionLedger.append_line(SessionLedger.savepoint_path(store, day), line, header: nil)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def run_savepoint(store, day, session, project, opts)
|
|
139
|
+
unless SessionLedger::EVENTS.include?(opts[:event])
|
|
140
|
+
usage_abort("--event must be one of #{SessionLedger::EVENTS.join(", ")}, got #{opts[:event].inspect}")
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
clean = sanitized_summary(opts[:summary])
|
|
144
|
+
line = SessionLedger.savepoint_line(opts[:event], session, project, clean, now: Time.now)
|
|
145
|
+
SessionLedger.append_line(SessionLedger.savepoint_path(store, day), line, header: nil)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def main(argv)
|
|
149
|
+
opts = parse_args(argv)
|
|
150
|
+
usage_abort("verb must be one of #{VERBS.join(", ")}, got #{opts[:verb].inspect}") unless VERBS.include?(opts[:verb])
|
|
151
|
+
|
|
152
|
+
plastic_home = expand(ENV.fetch("PLASTIC_HOME", "~/.plastic"))
|
|
153
|
+
store = opts[:store] ? expand(opts[:store]) : File.join(plastic_home, "store")
|
|
154
|
+
templates = opts[:templates] ? expand(opts[:templates]) : default_templates_dir
|
|
155
|
+
usage_abort("templates dir not found: #{templates}") unless Dir.exist?(templates)
|
|
156
|
+
cwd = opts[:cwd] ? expand(opts[:cwd]) : Dir.pwd
|
|
157
|
+
|
|
158
|
+
day = opts[:day] || SessionLedger.day_id
|
|
159
|
+
unless SessionLedger.valid_day_id?(day)
|
|
160
|
+
usage_abort("--day must be eight digits parsing as a real date, got #{day.inspect}")
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
if opts[:project] && !SessionLedger::SLUG_RE.match?(opts[:project])
|
|
164
|
+
usage_abort("--project must match [a-z0-9-]+, got #{opts[:project].inspect}")
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
session = SessionLedger.short_session_id(opts[:session], ENV["CLAUDE_CODE_SESSION_ID"])
|
|
168
|
+
project = opts[:project] || SessionLedger.project_slug(cwd, plastic_home: plastic_home)
|
|
169
|
+
|
|
170
|
+
# Always call open_day: it is idempotent (File::CREAT | File::EXCL decides
|
|
171
|
+
# create vs join) and cheap, so this never depends on Dir.exist?(day_dir)
|
|
172
|
+
# as a proxy for "does the day file exist". A guard keyed on the DIRECTORY
|
|
173
|
+
# missed the case of a day dir that exists without its <day>.md (a crashed
|
|
174
|
+
# scaffold, or a template-read failure): that guard would see the
|
|
175
|
+
# directory, skip open_day forever, and every append would land in a day
|
|
176
|
+
# dir with no intent file at all.
|
|
177
|
+
SessionLedger.open_day(store: store, day: day, templates: templates, author: session)
|
|
178
|
+
|
|
179
|
+
case opts[:verb]
|
|
180
|
+
when "pending"
|
|
181
|
+
append_checklist_line(:pending, store, day, session, project, opts[:summary])
|
|
182
|
+
when "item"
|
|
183
|
+
append_checklist_line(:open, store, day, session, project, opts[:summary])
|
|
184
|
+
when "promote"
|
|
185
|
+
run_transition(:pending, :open, "Item", store, day, session, project, opts)
|
|
186
|
+
when "tick"
|
|
187
|
+
run_transition(:open, :done, "Done", store, day, session, project, opts)
|
|
188
|
+
when "savepoint"
|
|
189
|
+
run_savepoint(store, day, session, project, opts)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
exit 0
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
main(ARGV) if $PROGRAM_NAME == __FILE__
|
package/scripts/codex-hook
CHANGED
|
@@ -2,42 +2,41 @@
|
|
|
2
2
|
# encoding: UTF-8
|
|
3
3
|
# frozen_string_literal: true
|
|
4
4
|
#
|
|
5
|
-
# Usage: codex-hook <
|
|
6
|
-
# file-mutation
|
|
7
|
-
# live-state hooks (intent 199
|
|
8
|
-
#
|
|
9
|
-
#
|
|
5
|
+
# Usage: codex-hook <hook>
|
|
6
|
+
# file-mutation hook: record (PostToolUse on apply_patch)
|
|
7
|
+
# live-state hooks (intent 199, capture/record collapse by 298):
|
|
8
|
+
# session-start | check-update | capture | savepoint
|
|
9
|
+
# session-end hook (intent 309): close, handed off detached
|
|
10
10
|
#
|
|
11
|
-
# The Codex input adapter (intent 102, extended by 199,
|
|
12
|
-
#
|
|
13
|
-
# name alone selects which path runs (Codex's own hooks.json
|
|
14
|
-
# guarantees they never cross): the apply_patch path reads
|
|
15
|
-
# level and the apply_patch command in tool_input.command
|
|
16
|
-
# parses the diff envelope once via ApplyPatchEnvelope
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
# (
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
#
|
|
36
|
-
#
|
|
37
|
-
#
|
|
38
|
-
#
|
|
39
|
-
# and
|
|
40
|
-
# could hold this dispatcher's pipe open past the launcher's own (fast) exit.
|
|
11
|
+
# The Codex input adapter (intent 102, extended by 199, collapsed by 251, cut to
|
|
12
|
+
# the record hook by 302). Reads a Codex hook stdin payload once. Two shapes
|
|
13
|
+
# exist and the hook name alone selects which path runs (Codex's own hooks.json
|
|
14
|
+
# registration guarantees they never cross): the apply_patch path reads
|
|
15
|
+
# session_id at top level and the apply_patch command in tool_input.command
|
|
16
|
+
# [guide Part 4] and parses the diff envelope once via ApplyPatchEnvelope; the
|
|
17
|
+
# live-state hooks carry no tool_input at all (they are not tool calls), so the
|
|
18
|
+
# dispatcher execs the SAME launcher file Claude already runs for that hook
|
|
19
|
+
# (hooks/<name>), which is already harness-agnostic (resolves ~/.plastic off
|
|
20
|
+
# $HOME, reads only the common stdin fields the guide confirms Codex shares with
|
|
21
|
+
# Claude for these events), so the body is reused, not rewritten (D3). The one
|
|
22
|
+
# adaptation on the live-state path is threading the payload's session_id into
|
|
23
|
+
# CLAUDE_CODE_SESSION_ID, since Codex's own process env never carries it, plus a
|
|
24
|
+
# bounded timeout: Codex invokes hooks synchronously, so a launcher that never
|
|
25
|
+
# releases this dispatcher's pipes would hang the session. hooks/check-update
|
|
26
|
+
# used to leak them by backgrounding an npm network call with the inherited
|
|
27
|
+
# descriptors (fixed at the root in intent 289 by redirecting that subshell to
|
|
28
|
+
# /dev/null); the bound stays for any other launcher that misbehaves, and
|
|
29
|
+
# capture_launcher keeps the timeout path silent.
|
|
30
|
+
#
|
|
31
|
+
# The PreToolUse edit-path gates and the Bash-matcher shell gate were removed in
|
|
32
|
+
# 2.0 (intent 302), and the power-tools reminder in intent 309. An older
|
|
33
|
+
# ~/.codex/hooks.json that still names one of them reaches the fail-open `else`
|
|
34
|
+
# below: exit 0, no output, no decision.
|
|
35
|
+
#
|
|
36
|
+
# SessionEnd (intent 309): Codex kills a SessionEnd hook after 3 seconds, so `close`
|
|
37
|
+
# is never relayed synchronously. The dispatcher hands the payload to hooks/close in a
|
|
38
|
+
# detached child (own process group, stdin from a pipe, stdout and stderr to /dev/null)
|
|
39
|
+
# and exits at once; the close hook's own detached filer does the slow part.
|
|
41
40
|
require "json"
|
|
42
41
|
require "open3"
|
|
43
42
|
require "rbconfig"
|
|
@@ -50,9 +49,55 @@ require "timeout"
|
|
|
50
49
|
# caller that merges the dispatcher's own stdout and stderr.
|
|
51
50
|
$stdout.sync = true
|
|
52
51
|
|
|
53
|
-
STATE_HOOKS = %w[session-start check-update
|
|
52
|
+
STATE_HOOKS = %w[session-start check-update capture savepoint close].freeze
|
|
54
53
|
STATE_TIMEOUT = 5
|
|
55
|
-
|
|
54
|
+
DETACHED_HOOKS = %w[close].freeze
|
|
55
|
+
|
|
56
|
+
# One capture path for the relayed-launcher branch (intent 289). The 5 s bound covers a
|
|
57
|
+
# launcher that leaks THIS process's stdout and stderr into a longer-lived background child
|
|
58
|
+
# (hooks/check-update did exactly that before 289): it does not bound a launcher that simply
|
|
59
|
+
# never exits in the foreground, since Open3.popen3's ensure joins the child's wait thread on
|
|
60
|
+
# the way out regardless of Timeout. What is added here is silence on the timeout path. A
|
|
61
|
+
# leaked-pipe launcher leaves Open3's reader threads blocked in IO#read; when Timeout unwinds
|
|
62
|
+
# and popen3's ensure closes those pipes underneath them, both threads raise IOError and Ruby
|
|
63
|
+
# prints a backtrace to real stderr, which any caller merging our stdout and stderr then reads
|
|
64
|
+
# as hook output. Thread#report_on_exception is seeded from the class default AT THREAD
|
|
65
|
+
# CREATION and capture3 creates its readers inside this block (open3.rb:664-665), so
|
|
66
|
+
# clearing the default here is enough. Restored in ensure: nothing else in this process,
|
|
67
|
+
# including a genuinely unexpected exception later, loses its report.
|
|
68
|
+
def capture_launcher(env, *argv, raw:, cwd:)
|
|
69
|
+
previous = Thread.report_on_exception
|
|
70
|
+
Thread.report_on_exception = false
|
|
71
|
+
begin
|
|
72
|
+
Timeout.timeout(STATE_TIMEOUT) { Open3.capture3(env, *argv, stdin_data: raw, chdir: cwd) }
|
|
73
|
+
rescue StandardError
|
|
74
|
+
["", "", nil] # fail open: launcher missing, unexecutable, crashed, or too slow
|
|
75
|
+
ensure
|
|
76
|
+
Thread.report_on_exception = previous
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Hand a launcher its payload and return without waiting (intent 309). The child gets its
|
|
81
|
+
# own process group so the harness's teardown of this dispatcher never reaches it, reads
|
|
82
|
+
# the payload from a pipe this process fills without blocking and closes, and writes
|
|
83
|
+
# nowhere. Every failure (a missing or unexecutable launcher, a closed pipe) is rescued
|
|
84
|
+
# silently, so nothing ever reaches Codex's stderr from this path.
|
|
85
|
+
def detach_launcher(env, launcher, raw:, cwd:)
|
|
86
|
+
reader, writer = IO.pipe
|
|
87
|
+
pid = Process.spawn(env, launcher, in: reader, out: File::NULL, err: File::NULL,
|
|
88
|
+
chdir: cwd, pgroup: true)
|
|
89
|
+
reader.close
|
|
90
|
+
begin
|
|
91
|
+
writer.write_nonblock(raw) # a hook payload fits the pipe buffer; a larger one is truncated
|
|
92
|
+
rescue IO::WaitWritable, SystemCallError
|
|
93
|
+
nil
|
|
94
|
+
ensure
|
|
95
|
+
writer.close
|
|
96
|
+
end
|
|
97
|
+
Process.detach(pid)
|
|
98
|
+
rescue StandardError
|
|
99
|
+
nil # fail open: a close that cannot start is a close the next-day sweep files
|
|
100
|
+
end
|
|
56
101
|
|
|
57
102
|
gate = ARGV[0].to_s
|
|
58
103
|
raw = ($stdin.read rescue nil)
|
|
@@ -67,31 +112,11 @@ if STATE_HOOKS.include?(gate)
|
|
|
67
112
|
cwd = payload["cwd"].to_s
|
|
68
113
|
cwd = Dir.pwd if cwd.empty? || !Dir.exist?(cwd)
|
|
69
114
|
env = { "CLAUDE_CODE_SESSION_ID" => (session.empty? ? nil : session) }
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
["", "", nil] # fail open: launcher missing, unexecutable, crashed, or too slow
|
|
74
|
-
end
|
|
75
|
-
print out unless out.to_s.empty?
|
|
76
|
-
warn err unless err.to_s.empty?
|
|
77
|
-
exit(status ? status.exitstatus : 0)
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
if SHELL_HOOKS.include?(gate)
|
|
81
|
-
# bash-gate (intent 203): a shell command has no apply_patch diff envelope, so
|
|
82
|
-
# this branch execs the SAME scripts/hook-bash-gate file Claude already runs,
|
|
83
|
-
# unmodified, and relays its exit code and stderr. It must never fall through
|
|
84
|
-
# to ApplyPatchEnvelope.parse below (see header comment).
|
|
85
|
-
cwd = payload["cwd"].to_s
|
|
86
|
-
cwd = Dir.pwd if cwd.empty? || !Dir.exist?(cwd)
|
|
87
|
-
env = { "CLAUDE_CODE_SESSION_ID" => (session.empty? ? nil : session) }
|
|
88
|
-
script = File.join(__dir__, "hook-#{gate}")
|
|
89
|
-
argv = [script]
|
|
90
|
-
out, err, status = begin
|
|
91
|
-
Timeout.timeout(STATE_TIMEOUT) { Open3.capture3(env.merge("RUBYOPT" => nil), RbConfig.ruby, *argv, stdin_data: raw, chdir: cwd) }
|
|
92
|
-
rescue StandardError
|
|
93
|
-
["", "", nil] # fail open: script missing, unexecutable, crashed, or too slow
|
|
115
|
+
if DETACHED_HOOKS.include?(gate)
|
|
116
|
+
detach_launcher(env.merge("RUBYOPT" => nil), launcher, raw: raw, cwd: cwd) if File.file?(launcher)
|
|
117
|
+
exit 0
|
|
94
118
|
end
|
|
119
|
+
out, err, status = capture_launcher(env.merge("RUBYOPT" => nil), launcher, raw: raw, cwd: cwd)
|
|
95
120
|
print out unless out.to_s.empty?
|
|
96
121
|
warn err unless err.to_s.empty?
|
|
97
122
|
exit(status ? status.exitstatus : 0)
|
|
@@ -100,48 +125,29 @@ end
|
|
|
100
125
|
require_relative "lib/apply_patch_envelope"
|
|
101
126
|
|
|
102
127
|
command = payload.dig("tool_input", "command") || payload.dig("tool_params", "command")
|
|
103
|
-
exit 0 if command.nil? # fail-open: no tool_input at all (unrecognized
|
|
128
|
+
exit 0 if command.nil? # fail-open: no tool_input at all (unrecognized hook name or non-tool-call payload)
|
|
104
129
|
ops = ApplyPatchEnvelope.parse(command)
|
|
105
|
-
exit 0 if ops.empty? # fail-open: nothing parseable to
|
|
130
|
+
exit 0 if ops.empty? # fail-open: nothing parseable to record
|
|
106
131
|
|
|
107
132
|
CORES = __dir__ # ~/.plastic/scripts
|
|
108
|
-
def run_core(name, *argv)
|
|
109
|
-
out = IO.popen([{ "RUBYOPT" => nil }, RbConfig.ruby, File.join(CORES, name), *argv], "r", err: [:child, :out], &:read)
|
|
110
|
-
[out, $?.exitstatus]
|
|
111
|
-
end
|
|
112
133
|
|
|
113
134
|
case gate
|
|
114
|
-
when "
|
|
115
|
-
#
|
|
116
|
-
#
|
|
117
|
-
#
|
|
118
|
-
#
|
|
119
|
-
#
|
|
120
|
-
#
|
|
121
|
-
|
|
122
|
-
require_relative "lib/codex_edit_gates"
|
|
123
|
-
exit CodexEditGates.dispatch(ops: ops, payload: payload)
|
|
124
|
-
rescue StandardError, ScriptError => e
|
|
125
|
-
# Dispatcher-internal failure: never impersonate a deny. ScriptError (a
|
|
126
|
-
# sibling of StandardError, not a subclass) covers LoadError: a future
|
|
127
|
-
# manifest gap (a lib file required here but not distributed, exactly the
|
|
128
|
-
# bug the full suite caught during this intent's own delivery) must fail
|
|
129
|
-
# open with a gate decision, never exit 1 with no decision at all.
|
|
130
|
-
$stderr.puts "plastic codex edit-gates error: #{e.message}"
|
|
131
|
-
exit 0
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
when "gate-check"
|
|
135
|
-
last_allow = nil
|
|
135
|
+
when "record"
|
|
136
|
+
# hook-record (intent 298) reads one stdin JSON payload; it takes no ARGV. One
|
|
137
|
+
# apply_patch call can bundle several file operations, so this synthesizes one
|
|
138
|
+
# Claude-shaped PostToolUse payload per op and pipes it to hook-record in-process,
|
|
139
|
+
# mirroring the "drive the body" pattern used for the live-state hooks. record
|
|
140
|
+
# never blocks and never prints anything (D3), so there is nothing to relay: every
|
|
141
|
+
# op's failure is independently rescued and the dispatcher always exits 0.
|
|
142
|
+
cwd_val = payload["cwd"].to_s
|
|
136
143
|
ops.each do |o|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
144
|
+
synth = JSON.generate("session_id" => session, "tool_input" => { "file_path" => o.path }, "cwd" => cwd_val)
|
|
145
|
+
begin
|
|
146
|
+
Open3.capture3({ "RUBYOPT" => nil }, RbConfig.ruby, File.join(CORES, "hook-record"), stdin_data: synth)
|
|
147
|
+
rescue StandardError
|
|
148
|
+
nil
|
|
141
149
|
end
|
|
142
|
-
last_allow = out unless out.to_s.strip.empty?
|
|
143
150
|
end
|
|
144
|
-
print last_allow if last_allow
|
|
145
151
|
exit 0
|
|
146
152
|
|
|
147
153
|
else
|
package/scripts/dashboard.rb
CHANGED
|
@@ -25,7 +25,7 @@ require "json"
|
|
|
25
25
|
require "yaml"
|
|
26
26
|
require "date"
|
|
27
27
|
require_relative "doctor"
|
|
28
|
-
require_relative "lib/
|
|
28
|
+
require_relative "lib/savepoint"
|
|
29
29
|
require_relative "lib/lock"
|
|
30
30
|
|
|
31
31
|
PLASTIC_HOME = ENV.fetch("PLASTIC_HOME") { File.join(Dir.home, ".plastic") }
|
|
@@ -36,7 +36,7 @@ def today
|
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
# A generic "about a month" threshold, not tuned to any one store's item count.
|
|
39
|
-
# Deliberately independent from plastic-
|
|
39
|
+
# Deliberately independent from plastic-intent-continuing's separate stale_threshold_days
|
|
40
40
|
# config: that one is a proactive boot-time triage nudge, this is a board annotation.
|
|
41
41
|
# Unifying the two is a follow-up, not this intent.
|
|
42
42
|
STALE_DAYS = 30
|
|
@@ -163,7 +163,7 @@ end
|
|
|
163
163
|
|
|
164
164
|
# True iff the savepoint ledger's last non-blank line shows real post-birth
|
|
165
165
|
# activity, not just the one-line birth stamp every intent gets at creation
|
|
166
|
-
# (scripts/new-intent's
|
|
166
|
+
# (scripts/new-intent's Savepoint.append_savepoint call, stage "What"). Reads the
|
|
167
167
|
# last line, extracts the stage token (second whitespace-separated field, same
|
|
168
168
|
# ledger shape last_accessed_at already parses), and treats any stage other
|
|
169
169
|
# than "What" as progress. Returns false when the file is missing/empty.
|
|
@@ -192,7 +192,7 @@ def parse_intent(store_info, dir_name, status_index)
|
|
|
192
192
|
# Sentinel-aware presence for lifecycle files (intent 60b): a scaffolded
|
|
193
193
|
# placeholder spec/plan/checklist/outcome reads as absent, so a freshly
|
|
194
194
|
# scaffolded intent reports What/Why and is never marked completed/advanced.
|
|
195
|
-
real = ->(f) {
|
|
195
|
+
real = ->(f) { Savepoint.stage_file_present?(File.join(dir, f)) }
|
|
196
196
|
body = File.exist?(md) ? File.read(md) : ""
|
|
197
197
|
|
|
198
198
|
status =
|