@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
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# hooks/message-display (intent 316a, O6, round 3 concurrency fix): the
|
|
3
|
+
# MessageDisplay launcher. Fires on every streamed chunk of every assistant
|
|
4
|
+
# message (D11), so the common case — an ordinary chunk of an ordinary
|
|
5
|
+
# message — must decide with shell builtins alone and fork nothing. Only a
|
|
6
|
+
# candidate message hands off to Ruby (scripts/hook-message-display), which
|
|
7
|
+
# is the one place allowed to do real work.
|
|
8
|
+
#
|
|
9
|
+
# No command substitution, no backticks, no sed/jq/cat: case, [, parameter
|
|
10
|
+
# expansion and printf are all builtins. Deliberately does NOT copy hooks/
|
|
11
|
+
# capture's SCRIPT_DIR-via-subshell pattern (cd into dirname of $0, inside a
|
|
12
|
+
# command substitution, then pwd) — that forks a subshell on every single
|
|
13
|
+
# invocation, which is exactly the cost this hook cannot carry.
|
|
14
|
+
#
|
|
15
|
+
# A live run under a real pty found Claude Code fires these chunk processes
|
|
16
|
+
# CONCURRENTLY: a chunk with index > 0 can arrive, and be judged here,
|
|
17
|
+
# before chunk 0 ever runs. The OLD hand-off test — "does a buffer already
|
|
18
|
+
# exist for this message" — answered no in that race and silently dropped
|
|
19
|
+
# the chunk before Ruby ever saw it, no matter what MessageDisplay's own
|
|
20
|
+
# (correct) polling logic would have done. So a later chunk is now also
|
|
21
|
+
# handed off when its OWN delta looks like it could be part of a screen
|
|
22
|
+
# (leading "|" or "**Steps**", or blank — the same cheap test Ruby itself
|
|
23
|
+
# uses to decide whether a wait is worth paying for), and the final chunk is
|
|
24
|
+
# ALWAYS handed off, whatever it looks like, since it is the one that must
|
|
25
|
+
# not race. Ruby is the one place that actually waits (bounded, injectable
|
|
26
|
+
# for tests); this script only ever decides once, fast, and never sleeps.
|
|
27
|
+
#
|
|
28
|
+
# Claude adapter: Claude Code only; the core is harness-agnostic.
|
|
29
|
+
IFS= read -r -d '' INPUT # returns 1 at EOF: do NOT set -e
|
|
30
|
+
case $INPUT in *'"message_id"'*) ;; *) exit 0 ;; esac
|
|
31
|
+
rest=${INPUT#*\"message_id\"}; rest=${rest#*\"}; mid=${rest%%\"*}
|
|
32
|
+
rest=${INPUT#*\"session_id\"}; rest=${rest#*\"}; sid=${rest%%\"*}
|
|
33
|
+
|
|
34
|
+
SCRIPT_DIR=${0%/*}
|
|
35
|
+
TMP_ROOT=${PLASTIC_TMP:-${TMPDIR:-/tmp}}
|
|
36
|
+
export PLASTIC_TMP="$TMP_ROOT"
|
|
37
|
+
MSGDIR="$TMP_ROOT/plastic-message-display/$sid/$mid"
|
|
38
|
+
|
|
39
|
+
is_index_zero=0
|
|
40
|
+
case $INPUT in *'"index":0'*|*'"index": 0'*) is_index_zero=1 ;; esac
|
|
41
|
+
|
|
42
|
+
is_final=0
|
|
43
|
+
case $INPUT in *'"final":true'*|*'"final": true'*) is_final=1 ;; esac
|
|
44
|
+
|
|
45
|
+
handoff=0
|
|
46
|
+
if [ "$is_index_zero" = 1 ]; then
|
|
47
|
+
# Chunk 0 decides synchronously; a bare "#" first delta, in either JSON
|
|
48
|
+
# spacing, is the only shape that can possibly open a screen. Must be a
|
|
49
|
+
# single "#", not "##" — a real screen's own first delta can be as short
|
|
50
|
+
# as "## " — a "##" glob would filter out exactly the message this hook
|
|
51
|
+
# exists to recognize.
|
|
52
|
+
# 317a (B11): a screen can open with "## " (state, delivered), a bare "▶"
|
|
53
|
+
# (roster), or a bare "✔" (delay); the JSON may carry the glyph raw or
|
|
54
|
+
# \u-escaped depending on the encoder. All globs are literal bytes - case
|
|
55
|
+
# matching is byte-wise, so multibyte literals are safe under bash 3.2.
|
|
56
|
+
case $INPUT in
|
|
57
|
+
*'"delta":"#'*|*'"delta": "#'*) handoff=1 ;;
|
|
58
|
+
*'"delta":"▶'*|*'"delta": "▶'*) handoff=1 ;;
|
|
59
|
+
*'"delta":"✔'*|*'"delta": "✔'*) handoff=1 ;;
|
|
60
|
+
*'"delta":"▶'*|*'"delta": "▶'*) handoff=1 ;;
|
|
61
|
+
*'"delta":"✔'*|*'"delta": "✔'*) handoff=1 ;;
|
|
62
|
+
esac
|
|
63
|
+
else
|
|
64
|
+
# A later chunk: hand off when this message's directory already exists
|
|
65
|
+
# (chunk 0 already left a decision or a chunk file), when this chunk is
|
|
66
|
+
# final (it must always be checked, whatever it looks like), or when its
|
|
67
|
+
# own delta is shaped like part of a screen — a Markdown table row, the
|
|
68
|
+
# "**Steps**" heading, or a blank line, in either JSON spacing.
|
|
69
|
+
[ -d "$MSGDIR" ] && handoff=1
|
|
70
|
+
[ "$is_final" = 1 ] && handoff=1
|
|
71
|
+
case $INPUT in
|
|
72
|
+
*'"delta":"|'*|*'"delta": "|'*) handoff=1 ;;
|
|
73
|
+
*'"delta":"**'*|*'"delta": "**'*) handoff=1 ;;
|
|
74
|
+
*'"delta":""'*|*'"delta": ""'*) handoff=1 ;;
|
|
75
|
+
*'"delta":"\n"'*|*'"delta": "\n"'*) handoff=1 ;;
|
|
76
|
+
esac
|
|
77
|
+
fi
|
|
78
|
+
|
|
79
|
+
[ "$handoff" = 1 ] || exit 0
|
|
80
|
+
|
|
81
|
+
printf '%s' "$INPUT" | env -u RUBYOPT ruby "$SCRIPT_DIR/../scripts/hook-message-display"
|
package/hooks/record
ADDED
package/hooks/savepoint
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
INPUT=""
|
|
3
|
+
[ -t 0 ] || INPUT=$(cat)
|
|
4
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
5
|
+
echo "$INPUT" | env -u RUBYOPT ruby "$SCRIPT_DIR/../scripts/hook-savepoint" "$HOME/.plastic"
|
|
6
|
+
exit 0
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zalom/plastic",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0-alpha.10",
|
|
4
4
|
"description": "Intent-driven idea development system for AI coding agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
"agents/",
|
|
39
39
|
"templates/",
|
|
40
40
|
"PLASTIC.md",
|
|
41
|
-
"deprecations.yml"
|
|
41
|
+
"deprecations.yml",
|
|
42
|
+
"config_asks.yml"
|
|
42
43
|
]
|
|
43
44
|
}
|
package/scripts/agent-report
CHANGED
|
@@ -21,7 +21,8 @@
|
|
|
21
21
|
#
|
|
22
22
|
# Exit codes: 0 (report emitted), 2 (usage).
|
|
23
23
|
|
|
24
|
-
require_relative "lib/
|
|
24
|
+
require_relative "lib/savepoint"
|
|
25
|
+
require_relative "lib/intent_screen"
|
|
25
26
|
|
|
26
27
|
def parse_args(argv)
|
|
27
28
|
role = nil
|
|
@@ -43,7 +44,7 @@ end
|
|
|
43
44
|
# Read the intent file frontmatter via the same path spawn-preamble uses, so the
|
|
44
45
|
# id/intent we report match what the rest of Plastic sees. Never raises.
|
|
45
46
|
def frontmatter_for(intent_dir)
|
|
46
|
-
ifile =
|
|
47
|
+
ifile = Savepoint.intent_file(intent_dir)
|
|
47
48
|
return {} unless File.exist?(ifile)
|
|
48
49
|
|
|
49
50
|
content = File.read(ifile)
|
|
@@ -68,20 +69,24 @@ STAGE_LABELS = {
|
|
|
68
69
|
"exec" => "Exec", "done" => "Done"
|
|
69
70
|
}.freeze
|
|
70
71
|
|
|
72
|
+
# Intent 317, D6: name the last LIFECYCLE line, never a trailing Lock/Review/
|
|
73
|
+
# Commit line (same guard as spawn-preamble; plan review finding B1).
|
|
71
74
|
def current_stage(intent_dir)
|
|
72
|
-
ledger = File.join(intent_dir,
|
|
75
|
+
ledger = File.join(intent_dir, Savepoint::SAVEPOINT_FILE)
|
|
73
76
|
if File.exist?(ledger)
|
|
74
|
-
|
|
75
|
-
|
|
77
|
+
lines = File.read(ledger).each_line.map(&:strip).reject(&:empty?)
|
|
78
|
+
lifecycle = lines.select { |l| IntentScreen.lifecycle_line?(l) }
|
|
79
|
+
return lifecycle.last if lifecycle.any?
|
|
80
|
+
return lines.last if lines.any?
|
|
76
81
|
end
|
|
77
|
-
STAGE_LABELS.fetch(
|
|
82
|
+
STAGE_LABELS.fetch(Savepoint.derive_stage(intent_dir), Savepoint.derive_stage(intent_dir))
|
|
78
83
|
end
|
|
79
84
|
|
|
80
85
|
# checklist.md checked/total: count GFM task-list items. Returns "n/a" when there
|
|
81
86
|
# is no checklist (a checklist is only meaningful from How onward).
|
|
82
87
|
def checklist_progress(intent_dir)
|
|
83
88
|
path = File.join(intent_dir, "checklist.md")
|
|
84
|
-
return "n/a" unless
|
|
89
|
+
return "n/a" unless Savepoint.stage_file_present?(path)
|
|
85
90
|
|
|
86
91
|
lines = File.readlines(path)
|
|
87
92
|
total = lines.count { |l| l =~ /^\s*- \[[ xX]\]/ }
|
|
@@ -95,7 +100,7 @@ end
|
|
|
95
100
|
# non-heading content line), else a placeholder. Pure read of the intent dir.
|
|
96
101
|
def outcome_line(intent_dir)
|
|
97
102
|
path = File.join(intent_dir, "outcome.md")
|
|
98
|
-
return "(no outcome yet)" unless
|
|
103
|
+
return "(no outcome yet)" unless Savepoint.stage_file_present?(path)
|
|
99
104
|
|
|
100
105
|
lines = File.readlines(path).map(&:rstrip)
|
|
101
106
|
start = lines.index { |l| l.strip.downcase == "## summary" }
|
|
@@ -112,7 +117,7 @@ end
|
|
|
112
117
|
# (the newest nugget, since entries are append-only newest-at-bottom), else
|
|
113
118
|
# `(none)`. Pure read of the intent dir, no clock.
|
|
114
119
|
def insights_line(intent_dir)
|
|
115
|
-
ifile =
|
|
120
|
+
ifile = Savepoint.intent_file(intent_dir)
|
|
116
121
|
return "(none)" unless File.exist?(ifile)
|
|
117
122
|
|
|
118
123
|
lines = File.read(ifile).split("\n", -1)
|
|
@@ -145,7 +150,7 @@ intent_name = "(unknown)" if intent_name.empty?
|
|
|
145
150
|
|
|
146
151
|
stage = current_stage(intent_dir)
|
|
147
152
|
role_label = (role && !role.empty?) ? role : stage
|
|
148
|
-
artifacts =
|
|
153
|
+
artifacts = Savepoint.has_files(intent_dir)
|
|
149
154
|
artifacts_str = artifacts.empty? ? "(none)" : artifacts.join(", ")
|
|
150
155
|
|
|
151
156
|
lines = []
|
|
@@ -0,0 +1,208 @@
|
|
|
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
|
+
require_relative "lib/handoff"
|
|
35
|
+
|
|
36
|
+
VERBS = %w[pending item promote tick savepoint].freeze
|
|
37
|
+
|
|
38
|
+
def usage_abort(message)
|
|
39
|
+
warn "append-ledger: #{message}"
|
|
40
|
+
exit 2
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# The value token that follows a flag at argv[i]. Aborts with exit 2 rather
|
|
44
|
+
# than silently returning nil when the flag is the last token (a trailing
|
|
45
|
+
# `--store` with nothing after it must not fall back to a default as if the
|
|
46
|
+
# flag had never been given).
|
|
47
|
+
def next_value!(argv, i, flag)
|
|
48
|
+
usage_abort("#{flag} requires a value") if i + 1 >= argv.length
|
|
49
|
+
argv[i + 1]
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def parse_args(argv)
|
|
53
|
+
opts = {
|
|
54
|
+
match: nil, savepoint: false, event: nil,
|
|
55
|
+
store: nil, day: nil, session: nil, project: nil, cwd: nil, templates: nil,
|
|
56
|
+
}
|
|
57
|
+
positional = []
|
|
58
|
+
i = 0
|
|
59
|
+
while i < argv.length
|
|
60
|
+
arg = argv[i]
|
|
61
|
+
case arg
|
|
62
|
+
when "--match" then opts[:match] = next_value!(argv, i, arg); i += 1
|
|
63
|
+
when "--event" then opts[:event] = next_value!(argv, i, arg); i += 1
|
|
64
|
+
when "--store" then opts[:store] = next_value!(argv, i, arg); i += 1
|
|
65
|
+
when "--day" then opts[:day] = next_value!(argv, i, arg); i += 1
|
|
66
|
+
when "--session" then opts[:session] = next_value!(argv, i, arg); i += 1
|
|
67
|
+
when "--project" then opts[:project] = next_value!(argv, i, arg); i += 1
|
|
68
|
+
when "--cwd" then opts[:cwd] = next_value!(argv, i, arg); i += 1
|
|
69
|
+
when "--templates" then opts[:templates] = next_value!(argv, i, arg); i += 1
|
|
70
|
+
when "--savepoint" then opts[:savepoint] = true
|
|
71
|
+
else
|
|
72
|
+
usage_abort("unknown flag #{arg.inspect}") if arg.start_with?("--")
|
|
73
|
+
|
|
74
|
+
positional << arg
|
|
75
|
+
end
|
|
76
|
+
i += 1
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
opts[:verb] = positional.shift
|
|
80
|
+
opts[:summary] = positional.shift
|
|
81
|
+
opts
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def expand(path)
|
|
85
|
+
File.expand_path(path.to_s.sub(/\A~/, Dir.home))
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Sibling of this script's dir. Works in-repo (<repo>/scripts/append-ledger
|
|
89
|
+
# -> <repo>/templates) and installed (~/.plastic/scripts/append-ledger ->
|
|
90
|
+
# ~/.plastic/templates when present).
|
|
91
|
+
def default_templates_dir
|
|
92
|
+
File.expand_path("../templates", __dir__)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def sanitized_summary(raw)
|
|
96
|
+
usage_abort("a summary is required") if raw.nil?
|
|
97
|
+
|
|
98
|
+
clean = SessionLedger.sanitize_summary(raw)
|
|
99
|
+
usage_abort("summary is empty after sanitizing") if clean.empty?
|
|
100
|
+
clean
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def append_checklist_line(state, store, day, session, project, summary)
|
|
104
|
+
clean = sanitized_summary(summary)
|
|
105
|
+
line = SessionLedger.checklist_line(state, session, project, clean)
|
|
106
|
+
path = SessionLedger.checklist_path(store, day)
|
|
107
|
+
SessionLedger.append_line(path, line, header: SessionLedger.checklist_header(day))
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# set_state identifies the target line AND flips it inside one LOCK_EX hold,
|
|
111
|
+
# returning the flipped line's own summary (or nil when nothing matched).
|
|
112
|
+
# There is deliberately no separate identify-then-flip step here: a prior
|
|
113
|
+
# version located the target under its own released lock, then set_state
|
|
114
|
+
# re-located it under a fresh lock, and the two scans could disagree under
|
|
115
|
+
# concurrency, naming the wrong line in the --savepoint entry below. Using
|
|
116
|
+
# set_state's return value is what keeps identify-and-flip atomic.
|
|
117
|
+
def run_transition(from, to, event, store, day, session, project, opts)
|
|
118
|
+
path = SessionLedger.checklist_path(store, day)
|
|
119
|
+
|
|
120
|
+
target_summary =
|
|
121
|
+
begin
|
|
122
|
+
SessionLedger.set_state(path, from: from, to: to, session: session, match: opts[:match])
|
|
123
|
+
rescue SessionLedger::LockUnavailableError => e
|
|
124
|
+
warn "append-ledger: #{e.message}"
|
|
125
|
+
exit 3
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
if target_summary.nil?
|
|
129
|
+
warn "append-ledger: nothing to address"
|
|
130
|
+
exit 2
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
if opts[:savepoint]
|
|
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
|
+
return unless to == :done
|
|
139
|
+
|
|
140
|
+
# The hand-off at every tick (intent 311, spec D4): derived and
|
|
141
|
+
# regenerable, so a failure here never fails the tick that was recorded.
|
|
142
|
+
begin
|
|
143
|
+
Handoff.write(store: store, day: day, session: session, trigger: "tick",
|
|
144
|
+
templates: opts[:templates] || default_templates_dir)
|
|
145
|
+
rescue StandardError
|
|
146
|
+
nil
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def run_savepoint(store, day, session, project, opts)
|
|
151
|
+
unless SessionLedger::EVENTS.include?(opts[:event])
|
|
152
|
+
usage_abort("--event must be one of #{SessionLedger::EVENTS.join(", ")}, got #{opts[:event].inspect}")
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
clean = sanitized_summary(opts[:summary])
|
|
156
|
+
line = SessionLedger.savepoint_line(opts[:event], session, project, clean, now: Time.now)
|
|
157
|
+
SessionLedger.append_line(SessionLedger.savepoint_path(store, day), line, header: nil)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def main(argv)
|
|
161
|
+
opts = parse_args(argv)
|
|
162
|
+
usage_abort("verb must be one of #{VERBS.join(", ")}, got #{opts[:verb].inspect}") unless VERBS.include?(opts[:verb])
|
|
163
|
+
|
|
164
|
+
plastic_home = expand(ENV.fetch("PLASTIC_HOME", "~/.plastic"))
|
|
165
|
+
store = opts[:store] ? expand(opts[:store]) : File.join(plastic_home, "store")
|
|
166
|
+
templates = opts[:templates] ? expand(opts[:templates]) : default_templates_dir
|
|
167
|
+
usage_abort("templates dir not found: #{templates}") unless Dir.exist?(templates)
|
|
168
|
+
opts[:templates] = templates
|
|
169
|
+
cwd = opts[:cwd] ? expand(opts[:cwd]) : Dir.pwd
|
|
170
|
+
|
|
171
|
+
day = opts[:day] || SessionLedger.day_id
|
|
172
|
+
unless SessionLedger.valid_day_id?(day)
|
|
173
|
+
usage_abort("--day must be eight digits parsing as a real date, got #{day.inspect}")
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
if opts[:project] && !SessionLedger::SLUG_RE.match?(opts[:project])
|
|
177
|
+
usage_abort("--project must match [a-z0-9-]+, got #{opts[:project].inspect}")
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
session = SessionLedger.short_session_id(opts[:session], ENV["CLAUDE_CODE_SESSION_ID"])
|
|
181
|
+
project = opts[:project] || SessionLedger.project_slug(cwd, plastic_home: plastic_home)
|
|
182
|
+
|
|
183
|
+
# Always call open_day: it is idempotent (File::CREAT | File::EXCL decides
|
|
184
|
+
# create vs join) and cheap, so this never depends on Dir.exist?(day_dir)
|
|
185
|
+
# as a proxy for "does the day file exist". A guard keyed on the DIRECTORY
|
|
186
|
+
# missed the case of a day dir that exists without its <day>.md (a crashed
|
|
187
|
+
# scaffold, or a template-read failure): that guard would see the
|
|
188
|
+
# directory, skip open_day forever, and every append would land in a day
|
|
189
|
+
# dir with no intent file at all.
|
|
190
|
+
SessionLedger.open_day(store: store, day: day, templates: templates, author: session)
|
|
191
|
+
|
|
192
|
+
case opts[:verb]
|
|
193
|
+
when "pending"
|
|
194
|
+
append_checklist_line(:pending, store, day, session, project, opts[:summary])
|
|
195
|
+
when "item"
|
|
196
|
+
append_checklist_line(:open, store, day, session, project, opts[:summary])
|
|
197
|
+
when "promote"
|
|
198
|
+
run_transition(:pending, :open, "Item", store, day, session, project, opts)
|
|
199
|
+
when "tick"
|
|
200
|
+
run_transition(:open, :done, "Done", store, day, session, project, opts)
|
|
201
|
+
when "savepoint"
|
|
202
|
+
run_savepoint(store, day, session, project, opts)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
exit 0
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
main(ARGV) if $PROGRAM_NAME == __FILE__
|
package/scripts/codex-hook
CHANGED
|
@@ -2,44 +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
|
-
#
|
|
40
|
-
# with the inherited descriptors (fixed at the root in intent 289 by
|
|
41
|
-
# redirecting that subshell to /dev/null); the bound stays for any other
|
|
42
|
-
# launcher that misbehaves, and capture_launcher keeps the timeout path silent.
|
|
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.
|
|
43
40
|
require "json"
|
|
44
41
|
require "open3"
|
|
45
42
|
require "rbconfig"
|
|
@@ -52,11 +49,11 @@ require "timeout"
|
|
|
52
49
|
# caller that merges the dispatcher's own stdout and stderr.
|
|
53
50
|
$stdout.sync = true
|
|
54
51
|
|
|
55
|
-
STATE_HOOKS = %w[session-start check-update
|
|
52
|
+
STATE_HOOKS = %w[session-start check-update capture savepoint close].freeze
|
|
56
53
|
STATE_TIMEOUT = 5
|
|
57
|
-
|
|
54
|
+
DETACHED_HOOKS = %w[close].freeze
|
|
58
55
|
|
|
59
|
-
# One capture path for
|
|
56
|
+
# One capture path for the relayed-launcher branch (intent 289). The 5 s bound covers a
|
|
60
57
|
# launcher that leaks THIS process's stdout and stderr into a longer-lived background child
|
|
61
58
|
# (hooks/check-update did exactly that before 289): it does not bound a launcher that simply
|
|
62
59
|
# never exits in the foreground, since Open3.popen3's ensure joins the child's wait thread on
|
|
@@ -80,6 +77,28 @@ def capture_launcher(env, *argv, raw:, cwd:)
|
|
|
80
77
|
end
|
|
81
78
|
end
|
|
82
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
|
|
101
|
+
|
|
83
102
|
gate = ARGV[0].to_s
|
|
84
103
|
raw = ($stdin.read rescue nil)
|
|
85
104
|
exit 0 if raw.nil? || raw.strip.empty?
|
|
@@ -93,73 +112,42 @@ if STATE_HOOKS.include?(gate)
|
|
|
93
112
|
cwd = payload["cwd"].to_s
|
|
94
113
|
cwd = Dir.pwd if cwd.empty? || !Dir.exist?(cwd)
|
|
95
114
|
env = { "CLAUDE_CODE_SESSION_ID" => (session.empty? ? nil : session) }
|
|
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
|
|
118
|
+
end
|
|
96
119
|
out, err, status = capture_launcher(env.merge("RUBYOPT" => nil), launcher, raw: raw, cwd: cwd)
|
|
97
120
|
print out unless out.to_s.empty?
|
|
98
121
|
warn err unless err.to_s.empty?
|
|
99
122
|
exit(status ? status.exitstatus : 0)
|
|
100
123
|
end
|
|
101
124
|
|
|
102
|
-
if SHELL_HOOKS.include?(gate)
|
|
103
|
-
# bash-gate (intent 203): a shell command has no apply_patch diff envelope, so
|
|
104
|
-
# this branch execs the SAME scripts/hook-bash-gate file Claude already runs,
|
|
105
|
-
# unmodified, and relays its exit code and stderr. It must never fall through
|
|
106
|
-
# to ApplyPatchEnvelope.parse below (see header comment).
|
|
107
|
-
cwd = payload["cwd"].to_s
|
|
108
|
-
cwd = Dir.pwd if cwd.empty? || !Dir.exist?(cwd)
|
|
109
|
-
env = { "CLAUDE_CODE_SESSION_ID" => (session.empty? ? nil : session) }
|
|
110
|
-
script = File.join(__dir__, "hook-#{gate}")
|
|
111
|
-
argv = [script]
|
|
112
|
-
out, err, status = capture_launcher(env.merge("RUBYOPT" => nil), RbConfig.ruby, *argv, raw: raw, cwd: cwd)
|
|
113
|
-
print out unless out.to_s.empty?
|
|
114
|
-
warn err unless err.to_s.empty?
|
|
115
|
-
exit(status ? status.exitstatus : 0)
|
|
116
|
-
end
|
|
117
|
-
|
|
118
125
|
require_relative "lib/apply_patch_envelope"
|
|
119
126
|
|
|
120
127
|
command = payload.dig("tool_input", "command") || payload.dig("tool_params", "command")
|
|
121
|
-
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)
|
|
122
129
|
ops = ApplyPatchEnvelope.parse(command)
|
|
123
|
-
exit 0 if ops.empty? # fail-open: nothing parseable to
|
|
130
|
+
exit 0 if ops.empty? # fail-open: nothing parseable to record
|
|
124
131
|
|
|
125
132
|
CORES = __dir__ # ~/.plastic/scripts
|
|
126
|
-
def run_core(name, *argv)
|
|
127
|
-
out = IO.popen([{ "RUBYOPT" => nil }, RbConfig.ruby, File.join(CORES, name), *argv], "r", err: [:child, :out], &:read)
|
|
128
|
-
[out, $?.exitstatus]
|
|
129
|
-
end
|
|
130
133
|
|
|
131
134
|
case gate
|
|
132
|
-
when "
|
|
133
|
-
#
|
|
134
|
-
#
|
|
135
|
-
#
|
|
136
|
-
#
|
|
137
|
-
#
|
|
138
|
-
#
|
|
139
|
-
|
|
140
|
-
require_relative "lib/codex_edit_gates"
|
|
141
|
-
exit CodexEditGates.dispatch(ops: ops, payload: payload)
|
|
142
|
-
rescue StandardError, ScriptError => e
|
|
143
|
-
# Dispatcher-internal failure: never impersonate a deny. ScriptError (a
|
|
144
|
-
# sibling of StandardError, not a subclass) covers LoadError: a future
|
|
145
|
-
# manifest gap (a lib file required here but not distributed, exactly the
|
|
146
|
-
# bug the full suite caught during this intent's own delivery) must fail
|
|
147
|
-
# open with a gate decision, never exit 1 with no decision at all.
|
|
148
|
-
$stderr.puts "plastic codex edit-gates error: #{e.message}"
|
|
149
|
-
exit 0
|
|
150
|
-
end
|
|
151
|
-
|
|
152
|
-
when "gate-check"
|
|
153
|
-
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
|
|
154
143
|
ops.each do |o|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
|
159
149
|
end
|
|
160
|
-
last_allow = out unless out.to_s.strip.empty?
|
|
161
150
|
end
|
|
162
|
-
print last_allow if last_allow
|
|
163
151
|
exit 0
|
|
164
152
|
|
|
165
153
|
else
|