@zalom/plastic 2.0.0-alpha.8 → 2.0.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 +13 -136
- package/README.md +357 -133
- package/agents/plastic-enforcer.md +20 -15
- package/agents/plastic-executor.md +15 -4
- package/agents/plastic-node-research.md +30 -0
- package/agents/plastic-node-verify.md +28 -0
- package/agents/plastic-node-work.md +33 -0
- package/agents/{plastic-advisor.md → plastic-primary-advisor.md} +7 -9
- package/agents/{plastic-faux-advisor.md → plastic-secondary-advisor.md} +9 -12
- package/assets/plastic-logo.svg +1 -0
- package/bin/crap +4 -0
- package/bin/lib/context_budget.rb +43 -6
- package/bin/lib/skill_census.rb +839 -0
- package/bin/plastic +6 -0
- package/bin/plastic-skill-census +114 -0
- package/bin/test +24 -4
- package/bin/verify-change +345 -0
- package/config_asks.yml +4 -4
- package/deprecations.yml +1 -1
- package/{skills/agent-advisor/references → docs/help}/advisor-protocol.md +19 -24
- package/{skills/auto/references → docs/help}/agent-architecture.md +16 -14
- package/{skills/conventions/references → docs/help}/completion-and-done.md +15 -16
- package/docs/help/human-report-contract.md +152 -0
- package/{skills/conventions/references → docs/help}/knowledge-graph.md +9 -0
- package/{skills/conventions/references → docs/help}/locks-and-worktrees.md +23 -13
- package/{skills/conventions/references → docs/help}/maintenance-and-revisions.md +1 -1
- package/{skills/conventions/references → docs/help}/roadmaps.md +2 -2
- package/{skills/tutorial/references → docs/help}/track-1-guided.md +28 -47
- package/{skills/tutorial/references → docs/help}/track-2-auto.md +8 -8
- package/{skills/tutorial/references → docs/help}/track-3-projects-and-roadmaps.md +24 -17
- package/hooks/call-budget +4 -0
- package/hooks/hooks.json +24 -0
- package/hooks/message-display +62 -1
- package/hooks/session-start +5 -1
- package/hooks/statusline +32 -27
- package/hooks/stop +5 -0
- package/package.json +5 -3
- package/scripts/append-ledger +2 -1
- package/scripts/dashboard.rb +267 -16
- package/scripts/day-summary +2 -1
- package/scripts/doctor.rb +680 -39
- package/scripts/end-intent +238 -22
- package/scripts/exec-worktree +5 -5
- package/scripts/file-session-intent +2 -1
- package/scripts/graph-measure +249 -0
- package/scripts/hook-call-budget +222 -0
- package/scripts/hook-capture +24 -123
- package/scripts/hook-close +2 -1
- package/scripts/hook-message-display +19 -5
- package/scripts/hook-record +24 -16
- package/scripts/hook-savepoint +27 -3
- package/scripts/hook-session-start +359 -321
- package/scripts/hook-stop +58 -0
- package/scripts/index-projection +74 -0
- package/scripts/insight-append +17 -4
- package/scripts/install.rb +9 -7
- package/scripts/lib/action_graph_shim.rb +279 -0
- package/scripts/lib/active_delivery.rb +105 -0
- package/scripts/lib/agent_models.rb +63 -25
- package/scripts/lib/arm.rb +48 -83
- package/scripts/lib/atomic_write.rb +31 -0
- package/scripts/lib/backup.rb +65 -0
- package/scripts/lib/cli/command.rb +92 -0
- package/scripts/lib/cli/commands/auto.rb +18 -0
- package/scripts/lib/cli/commands/auto_brief.rb +44 -0
- package/scripts/lib/cli/commands/auto_lock.rb +99 -0
- package/scripts/lib/cli/commands/auto_report.rb +50 -0
- package/scripts/lib/cli/commands/auto_take.rb +31 -0
- package/scripts/lib/cli/commands/backup.rb +43 -0
- package/scripts/lib/cli/commands/checkout.rb +25 -0
- package/scripts/lib/cli/commands/continue.rb +66 -0
- package/scripts/lib/cli/commands/doctor.rb +81 -0
- package/scripts/lib/cli/commands/feedback.rb +40 -0
- package/scripts/lib/cli/commands/help.rb +69 -0
- package/scripts/lib/cli/commands/hook.rb +32 -0
- package/scripts/lib/cli/commands/index.rb +23 -0
- package/scripts/lib/cli/commands/install.rb +21 -0
- package/scripts/lib/cli/commands/installer_verb.rb +37 -0
- package/scripts/lib/cli/commands/intent.rb +19 -0
- package/scripts/lib/cli/commands/intent_answer.rb +37 -0
- package/scripts/lib/cli/commands/intent_command.rb +53 -0
- package/scripts/lib/cli/commands/intent_end.rb +62 -0
- package/scripts/lib/cli/commands/intent_new.rb +62 -0
- package/scripts/lib/cli/commands/intent_note.rb +43 -0
- package/scripts/lib/cli/commands/intent_rule.rb +36 -0
- package/scripts/lib/cli/commands/intent_show.rb +32 -0
- package/scripts/lib/cli/commands/intent_spec.rb +44 -0
- package/scripts/lib/cli/commands/intent_step.rb +71 -0
- package/scripts/lib/cli/commands/intent_verify.rb +26 -0
- package/scripts/lib/cli/commands/migrate.rb +16 -0
- package/scripts/lib/cli/commands/migrate_stores.rb +31 -0
- package/scripts/lib/cli/commands/next.rb +54 -0
- package/scripts/lib/cli/commands/project.rb +19 -0
- package/scripts/lib/cli/commands/project_links.rb +42 -0
- package/scripts/lib/cli/commands/project_list.rb +20 -0
- package/scripts/lib/cli/commands/project_new.rb +72 -0
- package/scripts/lib/cli/commands/query.rb +31 -0
- package/scripts/lib/cli/commands/render.rb +33 -0
- package/scripts/lib/cli/commands/roadmap.rb +19 -0
- package/scripts/lib/cli/commands/roadmap_check.rb +54 -0
- package/scripts/lib/cli/commands/roadmap_log.rb +54 -0
- package/scripts/lib/cli/commands/roadmap_migrate.rb +53 -0
- package/scripts/lib/cli/commands/roadmap_next.rb +70 -0
- package/scripts/lib/cli/commands/roadmap_show.rb +45 -0
- package/scripts/lib/cli/commands/rollback.rb +20 -0
- package/scripts/lib/cli/commands/search.rb +60 -0
- package/scripts/lib/cli/commands/session.rb +18 -0
- package/scripts/lib/cli/commands/session_commit.rb +42 -0
- package/scripts/lib/cli/commands/session_handoff.rb +34 -0
- package/scripts/lib/cli/commands/session_summary.rb +35 -0
- package/scripts/lib/cli/commands/status.rb +68 -0
- package/scripts/lib/cli/commands/subcommand_list.rb +36 -0
- package/scripts/lib/cli/commands/sync.rb +46 -0
- package/scripts/lib/cli/commands/uninstall.rb +20 -0
- package/scripts/lib/cli/commands/update.rb +20 -0
- package/scripts/lib/cli/commands/version.rb +53 -0
- package/scripts/lib/cli/frontier.rb +86 -0
- package/scripts/lib/cli/intent_progress.rb +36 -0
- package/scripts/lib/cli/legacy.rb +80 -0
- package/scripts/lib/cli/output.rb +137 -0
- package/scripts/lib/cli/scope.rb +134 -0
- package/scripts/lib/cli/table.rb +65 -0
- package/scripts/lib/cli.rb +94 -0
- package/scripts/lib/codex_adapter.rb +198 -0
- package/scripts/lib/compact_instructions.rb +13 -5
- package/scripts/lib/core_integrity.rb +71 -0
- package/scripts/lib/dashboard_screen.rb +40 -0
- package/scripts/lib/data_boundary.rb +132 -0
- package/scripts/lib/day_summary.rb +30 -16
- package/scripts/lib/doctor_core.rb +114 -38
- package/scripts/lib/doctor_session_ledger.rb +5 -53
- package/scripts/lib/engine_permissions.rb +88 -0
- package/scripts/lib/exec_worktree.rb +24 -21
- package/scripts/lib/feedback_report.rb +1 -1
- package/scripts/lib/graph_edges.rb +137 -0
- package/scripts/lib/graph_file.rb +246 -0
- package/scripts/lib/graph_measure.rb +645 -0
- package/scripts/lib/graph_measure_budget.rb +409 -0
- package/scripts/lib/graph_measure_cohorts.rb +487 -0
- package/scripts/lib/graph_measure_models.rb +413 -0
- package/scripts/lib/graph_measure_report.rb +532 -0
- package/scripts/lib/graph_tree.rb +98 -0
- package/scripts/lib/guarded_append.rb +155 -0
- package/scripts/lib/handoff.rb +40 -13
- package/scripts/lib/harness_adapter.rb +184 -0
- package/scripts/lib/hook_registry.rb +27 -3
- package/scripts/lib/hook_replay.rb +229 -0
- package/scripts/lib/index_entry.rb +62 -0
- package/scripts/lib/index_projection.rb +201 -0
- package/scripts/lib/insights.rb +1 -1
- package/scripts/lib/installer_core.rb +532 -71
- package/scripts/lib/intent_screen.rb +9 -6
- package/scripts/lib/intent_screen_ansi.rb +73 -12
- package/scripts/lib/intent_validator.rb +2 -2
- package/scripts/lib/lock.rb +10 -11
- package/scripts/lib/message_display.rb +377 -173
- package/scripts/lib/meter_watch.rb +185 -0
- package/scripts/lib/node_file.rb +234 -0
- package/scripts/lib/node_ids.rb +99 -0
- package/scripts/lib/node_input.rb +913 -0
- package/scripts/lib/node_input_compatibility.rb +62 -0
- package/scripts/lib/node_ledger.rb +386 -0
- package/scripts/lib/node_progress.rb +153 -0
- package/scripts/lib/node_return.rb +204 -0
- package/scripts/lib/node_worktree.rb +337 -0
- package/scripts/lib/outcome_report.rb +440 -0
- package/scripts/lib/preflight.rb +4 -6
- package/scripts/lib/project_config.rb +46 -0
- package/scripts/lib/project_validator.rb +3 -2
- package/scripts/lib/qmd_sync.rb +8 -7
- package/scripts/lib/ready_set.rb +462 -0
- package/scripts/lib/reference_archive.rb +45 -0
- package/scripts/lib/release_guard.rb +18 -0
- package/scripts/lib/report_screen.rb +1460 -74
- package/scripts/lib/rlm/corpus.rb +13 -0
- package/scripts/lib/rlm/probe.rb +29 -0
- package/scripts/lib/rlm/query.rb +22 -0
- package/scripts/lib/roadmap_graph.rb +210 -0
- package/scripts/lib/roadmap_migration.rb +95 -0
- package/scripts/lib/roadmap_queue.rb +158 -8
- package/scripts/lib/roadmap_render.rb +150 -0
- package/scripts/lib/roadmap_savepoint.rb +64 -14
- package/scripts/lib/runner_absorb.rb +703 -0
- package/scripts/lib/runner_answer.rb +206 -0
- package/scripts/lib/runner_core.rb +194 -0
- package/scripts/lib/runner_dispatch.rb +525 -0
- package/scripts/lib/runner_policy.rb +191 -0
- package/scripts/lib/runner_proposals.rb +275 -0
- package/scripts/lib/runner_rewind.rb +201 -0
- package/scripts/lib/runner_sweep.rb +231 -0
- package/scripts/lib/runner_until_empty.rb +252 -0
- package/scripts/lib/runner_watch.rb +389 -0
- package/scripts/lib/savepoint.rb +141 -19
- package/scripts/lib/scaffold_intent.rb +6 -3
- package/scripts/lib/screen_paint.rb +555 -0
- package/scripts/lib/screens/dashboard.rb +20 -0
- package/scripts/lib/screens/plan.rb +18 -0
- package/scripts/lib/screens/roadmap.rb +15 -0
- package/scripts/lib/search_index.rb +55 -0
- package/scripts/lib/session_close.rb +30 -28
- package/scripts/lib/session_git.rb +25 -18
- package/scripts/lib/session_ledger.rb +48 -4
- package/scripts/lib/session_usage.rb +190 -0
- package/scripts/lib/sqlite.rb +22 -0
- package/scripts/lib/stop_gate.rb +95 -0
- package/scripts/lib/store_discovery.rb +7 -6
- package/scripts/lib/store_layout.rb +54 -0
- package/scripts/lib/store_provisioning.rb +2 -1
- package/scripts/lib/store_sync.rb +85 -0
- package/scripts/lib/stores_move.rb +93 -0
- package/scripts/lib/verify_intent.rb +36 -8
- package/scripts/lib/version_number.rb +48 -0
- package/scripts/lib/work_graph.rb +59 -0
- package/scripts/lib/work_graph_validator.rb +201 -0
- package/scripts/lib/worktree.rb +27 -32
- package/scripts/lib/worktree_sweep.rb +6 -5
- package/scripts/link-suggest +2 -1
- package/scripts/meter-watch +57 -0
- package/scripts/migrate-to-global +1 -1
- package/scripts/new-intent +4 -13
- package/scripts/node-input +92 -0
- package/scripts/node-run +225 -0
- package/scripts/node-transition +291 -0
- package/scripts/outcome-report +74 -0
- package/scripts/plastic-lock +42 -33
- package/scripts/promote-session-item +3 -2
- package/scripts/read-config +53 -9
- package/scripts/ready-set +126 -0
- package/scripts/release-check +123 -0
- package/scripts/report-screen +192 -30
- package/scripts/roadmap-graph +119 -0
- package/scripts/roadmap-savepoint +7 -0
- package/scripts/runner +581 -0
- package/scripts/savepoint-note +11 -9
- package/scripts/session-commit +2 -1
- package/scripts/session-usage +56 -0
- package/scripts/skill-lint +115 -6
- package/scripts/spawn-preamble +2 -2
- package/scripts/update.rb +25 -4
- package/scripts/validate-work-graph +39 -0
- package/scripts/verify-intent +3 -2
- package/scripts/write-handoff +2 -1
- package/templates/agents.md +7 -7
- package/templates/config.yml +16 -9
- package/templates/dashboard-screen.md +22 -0
- package/templates/display-fixture.md +21 -0
- package/templates/graph.md +16 -0
- package/templates/index.md +1 -1
- package/templates/intent-screen.md +1 -1
- package/templates/node-decision.md +11 -0
- package/templates/node-research.md +13 -0
- package/templates/node-verify.md +13 -0
- package/templates/node-work.md +22 -0
- package/templates/outcome.md +15 -2
- package/templates/project.yml +1 -1
- package/templates/render.css +10 -0
- package/templates/report-plan.md +15 -0
- package/templates/report-roadmap-delivered.md +10 -0
- package/templates/report-roadmap-plan.md +9 -0
- package/templates/report-roadmap-state.md +9 -0
- package/templates/report-state.md +1 -1
- package/templates/roadmap.md +13 -0
- package/bin/plastic.js +0 -70
- package/scripts/lib/bridge.rb +0 -116
- package/skills/agent-advisor/SKILL.md +0 -92
- package/skills/auto/SKILL.md +0 -295
- package/skills/auto/evals/evals.json +0 -255
- package/skills/auto/references/end-tail.md +0 -66
- package/skills/auto/references/human-report-contract.md +0 -78
- package/skills/conventions/SKILL.md +0 -29
- package/skills/dashboard/SKILL.md +0 -169
- package/skills/dashboard/evals/evals.json +0 -38
- package/skills/dashboard/references/classification.md +0 -22
- package/skills/dashboard/templates/dashboard-global.md +0 -20
- package/skills/dashboard/templates/dashboard-project.md +0 -19
- package/skills/direct/SKILL.md +0 -66
- package/skills/direct/references/request-signals.md +0 -59
- package/skills/doctor/SKILL.md +0 -299
- package/skills/doctor/report.md +0 -102
- package/skills/feedback/SKILL.md +0 -98
- package/skills/feedback/references/transport-and-privacy.md +0 -65
- package/skills/feedback/report.md +0 -36
- package/skills/install/SKILL.md +0 -217
- package/skills/intent-continuing/SKILL.md +0 -154
- package/skills/intent-continuing/references/board-fill.md +0 -43
- package/skills/intent-continuing/references/boarding-matrix.md +0 -34
- package/skills/intent-continuing/references/context-management.md +0 -28
- package/skills/intent-continuing/references/liveness-ranking.md +0 -57
- package/skills/intent-creating/SKILL.md +0 -164
- package/skills/intent-creating/evals/evals.json +0 -72
- package/skills/intent-creating/references/lifecycle.md +0 -81
- package/skills/intent-creating/references/wikilinks.md +0 -8
- package/skills/intent-ending/SKILL.md +0 -173
- package/skills/intent-ending/evals/evals.json +0 -74
- package/skills/intent-executing/SKILL.md +0 -170
- package/skills/intent-executing/evals/evals.json +0 -66
- package/skills/intent-executing/implementer-prompt.md +0 -42
- package/skills/intent-executing/spec-reviewer-prompt.md +0 -27
- package/skills/intent-speccing/SKILL.md +0 -130
- package/skills/intent-speccing/evals/evals.json +0 -126
- package/skills/intent-speccing/references/design-principles.md +0 -44
- package/skills/intent-speccing/references/per-section-fill-rules.md +0 -92
- package/skills/intent-speccing/references/self-verify-checklist.md +0 -37
- package/skills/project-creating/SKILL.md +0 -162
- package/skills/project-creating/references/hubs-projects.md +0 -55
- package/skills/project-creating/references/project-scaffolding.md +0 -97
- package/skills/releasing/SKILL.md +0 -337
- package/skills/releasing/references/deprecations.md +0 -60
- package/skills/releasing/references/promotion-and-tagging.md +0 -66
- package/skills/releasing/references/release-lines.md +0 -105
- package/skills/roadmap/SKILL.md +0 -64
- package/skills/roadmap/references/file-format.md +0 -124
- package/skills/roadmap/references/operations.md +0 -112
- package/skills/rollback/SKILL.md +0 -91
- package/skills/tutorial/SKILL.md +0 -65
- package/skills/tutorial/evals/evals.json +0 -186
- package/skills/uninstall/SKILL.md +0 -75
- package/skills/update/SKILL.md +0 -126
- /package/{skills/auto/references → docs/help}/agent-report-contract.md +0 -0
- /package/{skills/intent-executing → docs/help}/code-quality-reviewer-prompt.md +0 -0
- /package/{skills/conventions/references → docs/help}/lifecycle-and-savepoints.md +0 -0
- /package/{skills/intent-executing → docs/help}/plan-reviewer-prompt.md +0 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# GuardedAppend - the shared fail-closed write guard behind NodeLedger's transition
|
|
5
|
+
# lines and RoadmapSavepoint's roadmap ledger (intent 335, spec "Approach").
|
|
6
|
+
#
|
|
7
|
+
# One module function, #call: opens `path` RDWR|APPEND|CREAT, takes a non-blocking
|
|
8
|
+
# exclusive lock with a bounded number of retries, and under that ONE hold reads the
|
|
9
|
+
# current content, yields it to the caller's block, and appends whatever the block
|
|
10
|
+
# returns. A block returning nil is a refusal: nothing is written. Read, decide, and
|
|
11
|
+
# append happen inside one lock hold on purpose (spec "Approach"): that is what makes
|
|
12
|
+
# "is this subject already running" and "append running" atomic against a second
|
|
13
|
+
# writer, which a check followed by a separate append could never be.
|
|
14
|
+
#
|
|
15
|
+
# Pure and dependency-injected: `flock:` and `sleeper:` are constructor-style test
|
|
16
|
+
# seams (never an environment variable, never a global); this module reads no
|
|
17
|
+
# environment variable and shells out to nothing.
|
|
18
|
+
module GuardedAppend
|
|
19
|
+
# Raised when the lock could not be taken within `retries` attempts (real
|
|
20
|
+
# contention), or when flock itself is unsupported on this filesystem and
|
|
21
|
+
# `strict: true` (spec D12a). Nothing is written either way; the caller is told
|
|
22
|
+
# plainly that nothing landed and must retry.
|
|
23
|
+
class Unavailable < StandardError; end
|
|
24
|
+
|
|
25
|
+
module_function
|
|
26
|
+
|
|
27
|
+
# Five attempts, 20 ms apart: about 100 ms of wall time total (spec D12).
|
|
28
|
+
DEFAULT_RETRIES = 5
|
|
29
|
+
DEFAULT_BACKOFF = 0.02
|
|
30
|
+
|
|
31
|
+
# Default lock and sleep seams: a real flock call, a real sleep. Tests inject
|
|
32
|
+
# replacements to simulate contention, recovery, and a flock-less filesystem
|
|
33
|
+
# hermetically, with no need for a real flock-less mount or a slow test run.
|
|
34
|
+
DEFAULT_FLOCK = ->(handle, mode) { handle.flock(mode) }
|
|
35
|
+
private_constant :DEFAULT_FLOCK
|
|
36
|
+
|
|
37
|
+
DEFAULT_SLEEPER = ->(seconds) { sleep(seconds) }
|
|
38
|
+
private_constant :DEFAULT_SLEEPER
|
|
39
|
+
|
|
40
|
+
# Open `path` (creating it if absent, never truncating it), take an exclusive
|
|
41
|
+
# non-blocking lock with up to `retries` attempts (`backoff` seconds apart), and
|
|
42
|
+
# under that one hold read the file's current content, yield it to the block, and
|
|
43
|
+
# append what the block returns.
|
|
44
|
+
#
|
|
45
|
+
# Returns :written when a line was appended, :refused when the block returned nil
|
|
46
|
+
# (nothing written, spec: a refusal). Raises Unavailable, writing nothing, when the
|
|
47
|
+
# lock could not be taken within `retries` attempts.
|
|
48
|
+
#
|
|
49
|
+
# `strict:` decides what happens when flock itself raises a SystemCallError OTHER
|
|
50
|
+
# than contention (EWOULDBLOCK/EAGAIN) - a filesystem without flock support,
|
|
51
|
+
# distinct from real contention (spec D12a): strict (the default) raises
|
|
52
|
+
# Unavailable; non-strict proceeds unguarded, since a single O_APPEND write still
|
|
53
|
+
# lands whole there. The SystemCallError rescue wraps the flock call only (spec
|
|
54
|
+
# D12b); an Errno::ENOENT from File.open (a missing parent directory) propagates
|
|
55
|
+
# as itself, never read as Unavailable.
|
|
56
|
+
def call(path, retries: DEFAULT_RETRIES, backoff: DEFAULT_BACKOFF, strict: true,
|
|
57
|
+
flock: DEFAULT_FLOCK, sleeper: DEFAULT_SLEEPER, &block)
|
|
58
|
+
created = !File.exist?(path)
|
|
59
|
+
handle = File.open(path, File::RDWR | File::APPEND | File::CREAT, 0o644)
|
|
60
|
+
begin
|
|
61
|
+
status = take_lock(handle, retries: retries, backoff: backoff, flock: flock, sleeper: sleeper)
|
|
62
|
+
|
|
63
|
+
case status
|
|
64
|
+
when :contended
|
|
65
|
+
raise Unavailable, "could not take an exclusive lock on #{path} after #{retries} attempts"
|
|
66
|
+
when :unsupported
|
|
67
|
+
if strict
|
|
68
|
+
raise Unavailable, "flock is unsupported on #{path} and strict: true refuses to proceed unguarded"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
write_line(handle, &block)
|
|
72
|
+
when :locked
|
|
73
|
+
begin
|
|
74
|
+
write_line(handle, &block)
|
|
75
|
+
ensure
|
|
76
|
+
unlock(handle, flock: flock)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
ensure
|
|
80
|
+
handle.close
|
|
81
|
+
end
|
|
82
|
+
rescue Unavailable
|
|
83
|
+
remove_freshly_created_empty_file(path) if created
|
|
84
|
+
raise
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Attempt the lock up to `retries` times. Returns :locked, :unsupported (a
|
|
88
|
+
# non-contention SystemCallError from flock, decided once, never retried), or
|
|
89
|
+
# :contended (every attempt failed with EWOULDBLOCK/EAGAIN or a false return).
|
|
90
|
+
# Sleeps `backoff` seconds after EVERY contended attempt, including the last, so
|
|
91
|
+
# the total backoff budget is exactly `retries` sleeps (spec D12: "about 100 ms of
|
|
92
|
+
# wall time in total" = 5 attempts * 20 ms, not 4).
|
|
93
|
+
def take_lock(handle, retries:, backoff:, flock:, sleeper:)
|
|
94
|
+
status = :contended
|
|
95
|
+
retries.times do
|
|
96
|
+
status = try_flock(handle, flock)
|
|
97
|
+
return status unless status == :contended
|
|
98
|
+
|
|
99
|
+
sleeper.call(backoff)
|
|
100
|
+
end
|
|
101
|
+
status
|
|
102
|
+
end
|
|
103
|
+
private_class_method :take_lock
|
|
104
|
+
|
|
105
|
+
# One attempt at the non-blocking exclusive lock. File#flock RAISES (does not
|
|
106
|
+
# return false) for every errno except EWOULDBLOCK/EAGAIN on most platforms, so
|
|
107
|
+
# both the "returns false" and the "raises EWOULDBLOCK" shapes read as contention;
|
|
108
|
+
# any other SystemCallError means flock is not supported on this filesystem.
|
|
109
|
+
def try_flock(handle, flock)
|
|
110
|
+
result = flock.call(handle, File::LOCK_EX | File::LOCK_NB)
|
|
111
|
+
result == false ? :contended : :locked
|
|
112
|
+
rescue Errno::EWOULDBLOCK, Errno::EAGAIN
|
|
113
|
+
:contended
|
|
114
|
+
rescue SystemCallError
|
|
115
|
+
:unsupported
|
|
116
|
+
end
|
|
117
|
+
private_class_method :try_flock
|
|
118
|
+
|
|
119
|
+
# Read the current content, yield it to the block, and write what it returns. A
|
|
120
|
+
# block returning nil writes nothing and reports :refused. Before a real write, if
|
|
121
|
+
# the content is non-empty and does not end in a newline, a newline is written
|
|
122
|
+
# first (spec D9a): a crash that truncated the previous write must not glue the
|
|
123
|
+
# next transition onto its tail. The handle is opened O_APPEND, so every write
|
|
124
|
+
# lands at the current end of file regardless of the read's cursor position.
|
|
125
|
+
def write_line(handle, &block)
|
|
126
|
+
content = handle.read
|
|
127
|
+
line = block.call(content)
|
|
128
|
+
return :refused if line.nil?
|
|
129
|
+
|
|
130
|
+
prefix = !content.empty? && !content.end_with?("\n") ? "\n" : ""
|
|
131
|
+
handle.write("#{prefix}#{line}")
|
|
132
|
+
handle.flush
|
|
133
|
+
:written
|
|
134
|
+
end
|
|
135
|
+
private_class_method :write_line
|
|
136
|
+
|
|
137
|
+
def unlock(handle, flock:)
|
|
138
|
+
flock.call(handle, File::LOCK_UN)
|
|
139
|
+
rescue SystemCallError
|
|
140
|
+
nil
|
|
141
|
+
end
|
|
142
|
+
private_class_method :unlock
|
|
143
|
+
|
|
144
|
+
# Row 7.9 (post-execution review): a give-up (Unavailable) must not leave a zero-byte
|
|
145
|
+
# file behind that File::CREAT created for a target that did not exist before this
|
|
146
|
+
# call. Only ever removes a file this same call created (never a pre-existing file,
|
|
147
|
+
# spec matrix 1.11) and only when it is still empty (no write ever reached it on the
|
|
148
|
+
# give-up paths this rescues).
|
|
149
|
+
def remove_freshly_created_empty_file(path)
|
|
150
|
+
File.unlink(path) if File.exist?(path) && File.zero?(path)
|
|
151
|
+
rescue SystemCallError
|
|
152
|
+
nil
|
|
153
|
+
end
|
|
154
|
+
private_class_method :remove_freshly_created_empty_file
|
|
155
|
+
end
|
package/scripts/lib/handoff.rb
CHANGED
|
@@ -19,6 +19,10 @@ module Handoff
|
|
|
19
19
|
DONE_CAP = 10
|
|
20
20
|
RECENT_CAP = 10
|
|
21
21
|
OTHERS_CAP = 10
|
|
22
|
+
# Intent 340b (G7c, n4, D11/D12): runner-step.last is raw stdout+stderr,
|
|
23
|
+
# not a bounded list of items like every other section, so it gets a byte
|
|
24
|
+
# cap of its own rather than an item count.
|
|
25
|
+
RUNNER_CAP = 2048
|
|
22
26
|
# A ledger summary may run to 200 characters; a hand-off line shows the
|
|
23
27
|
# first 80, so the caps above are reachable inside the byte budget.
|
|
24
28
|
SUMMARY_MAX = 80
|
|
@@ -34,15 +38,11 @@ module Handoff
|
|
|
34
38
|
File.join(SessionLedger.day_dir(store, day), "handoff--#{session}.md")
|
|
35
39
|
end
|
|
36
40
|
|
|
37
|
-
# The day this session's hand-off belongs to: the
|
|
38
|
-
#
|
|
39
|
-
#
|
|
41
|
+
# The day this session's hand-off belongs to: the session's day from the
|
|
42
|
+
# day ledger (SessionLedger.session_day, graph.md D7), the same lookup
|
|
43
|
+
# SessionClose uses for the drop at close.
|
|
40
44
|
def day_for(store, session, today:)
|
|
41
|
-
|
|
42
|
-
return today unless File.exist?(path)
|
|
43
|
-
|
|
44
|
-
value = File.read(path).strip
|
|
45
|
-
SessionLedger.valid_day_id?(value) ? value : today
|
|
45
|
+
SessionLedger.session_day(store, session, today: today)
|
|
46
46
|
end
|
|
47
47
|
|
|
48
48
|
def clip(summary)
|
|
@@ -74,9 +74,34 @@ module Handoff
|
|
|
74
74
|
end
|
|
75
75
|
end
|
|
76
76
|
|
|
77
|
+
# Raw text of the delivering intent's runner-step.last, or nil when there
|
|
78
|
+
# is no intent, the file is absent, or it cannot be read (row 4.41: a torn
|
|
79
|
+
# file must never raise a PreCompact hand-off out of existence).
|
|
80
|
+
def read_runner_last(intent)
|
|
81
|
+
return nil if intent.to_s.strip.empty?
|
|
82
|
+
|
|
83
|
+
path = File.join(intent, "runner-step.last")
|
|
84
|
+
return nil unless File.file?(path)
|
|
85
|
+
|
|
86
|
+
text = File.read(path)
|
|
87
|
+
text.strip.empty? ? nil : text
|
|
88
|
+
rescue StandardError
|
|
89
|
+
nil
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Omitted entirely when no intent resolves or the file is absent/empty
|
|
93
|
+
# (row 4.39); capped like every other section otherwise (row 4.40).
|
|
94
|
+
def runner_section(intent)
|
|
95
|
+
text = read_runner_last(intent)
|
|
96
|
+
return nil unless text
|
|
97
|
+
|
|
98
|
+
clipped = text.bytesize > RUNNER_CAP ? "#{text.byteslice(0, RUNNER_CAP)}\n(truncated)" : text
|
|
99
|
+
"## Runner\n#{clipped}\n"
|
|
100
|
+
end
|
|
101
|
+
|
|
77
102
|
# --- rendering, pure -----------------------------------------------------------
|
|
78
103
|
|
|
79
|
-
def render(store:, day:, session:, trigger:, now: Time.now)
|
|
104
|
+
def render(store:, day:, session:, trigger:, now: Time.now, intent: nil)
|
|
80
105
|
raise ArgumentError, "unknown trigger: #{trigger.inspect}" unless TRIGGERS.include?(trigger)
|
|
81
106
|
|
|
82
107
|
items = read_items(store, day)
|
|
@@ -102,8 +127,9 @@ module Handoff
|
|
|
102
127
|
"Written #{now.utc.strftime('%Y-%m-%dT%H:%M:%SZ')} at #{trigger}",
|
|
103
128
|
"",
|
|
104
129
|
]
|
|
130
|
+
runner = runner_section(intent)
|
|
105
131
|
loop do
|
|
106
|
-
text = compose(header, lists, hidden)
|
|
132
|
+
text = compose(header, lists, hidden, runner)
|
|
107
133
|
return text if text.bytesize <= BUDGET
|
|
108
134
|
|
|
109
135
|
key = TRIM_ORDER.find { |k| !lists[k].empty? }
|
|
@@ -123,12 +149,13 @@ module Handoff
|
|
|
123
149
|
lists[key] = lists[key].last(cap)
|
|
124
150
|
end
|
|
125
151
|
|
|
126
|
-
def compose(header, lists, hidden)
|
|
152
|
+
def compose(header, lists, hidden, runner = nil)
|
|
127
153
|
sections = [
|
|
128
154
|
section("Open", lists[:open], hidden[:open]),
|
|
129
155
|
section("Done", lists[:done], hidden[:done]),
|
|
130
156
|
section("Recent", lists[:recent], hidden[:recent]),
|
|
131
157
|
section("Others today", lists[:others], hidden[:others]),
|
|
158
|
+
runner,
|
|
132
159
|
"## Resume\n#{RESUME}\n",
|
|
133
160
|
]
|
|
134
161
|
(header + sections.compact).join("\n")
|
|
@@ -168,13 +195,13 @@ module Handoff
|
|
|
168
195
|
# partial hand-off and two writers for one session (a tick overlapping a
|
|
169
196
|
# PreCompact) never share a temp name. Returns the path. With
|
|
170
197
|
# `templates: nil` the day is not scaffolded, only its directory ensured.
|
|
171
|
-
def write(store:, day:, session:, trigger:, templates:, now: Time.now)
|
|
198
|
+
def write(store:, day:, session:, trigger:, templates:, now: Time.now, intent: nil)
|
|
172
199
|
if templates
|
|
173
200
|
SessionLedger.open_day(store: store, day: day, templates: templates, author: session)
|
|
174
201
|
else
|
|
175
202
|
FileUtils.mkdir_p(SessionLedger.day_dir(store, day))
|
|
176
203
|
end
|
|
177
|
-
text = render(store: store, day: day, session: session, trigger: trigger, now: now)
|
|
204
|
+
text = render(store: store, day: day, session: session, trigger: trigger, now: now, intent: intent)
|
|
178
205
|
target = path_for(store, day, session)
|
|
179
206
|
tmp = File.join(File.dirname(target), ".handoff-#{session}-#{Process.pid}-#{Thread.current.object_id}.tmp")
|
|
180
207
|
File.write(tmp, text)
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# HarnessAdapter (intent 340b, G7c, n1): the one module in Plastic that knows
|
|
5
|
+
# a harness by name. It resolves the current key from config `agent.type`,
|
|
6
|
+
# renders a dispatch plan into that harness's instructions, and answers what
|
|
7
|
+
# the ledger should record. `runner step` calls it; nothing else in the
|
|
8
|
+
# runner does (spec D1).
|
|
9
|
+
#
|
|
10
|
+
# Pure: every input is an argument, nothing here reads ENV or a real
|
|
11
|
+
# config.yml itself - the caller (scripts/runner) owns loading real config,
|
|
12
|
+
# this module only resolves values out of what it is handed, the same
|
|
13
|
+
# discipline RunnerPolicy already holds for model resolution.
|
|
14
|
+
module HarnessAdapter
|
|
15
|
+
module_function
|
|
16
|
+
|
|
17
|
+
# The only two harnesses this module knows (spec D2). An unknown key -
|
|
18
|
+
# config-authored or from `--harness` - always falls back to this rather
|
|
19
|
+
# than to no adapter at all (matrix row 1.3): a plan with no rendering
|
|
20
|
+
# under it is worse than a plan rendered for the wrong harness, because the
|
|
21
|
+
# first is silent.
|
|
22
|
+
DEFAULT_KEY = "claude-code"
|
|
23
|
+
KNOWN_KEYS = %w[claude-code codex].freeze
|
|
24
|
+
|
|
25
|
+
# The exact sentence 340a's watch prints to stderr and exits 1 with,
|
|
26
|
+
# before any write, when `--dispatch` is asked of a harness
|
|
27
|
+
# #unattended_start? refuses (graph.md D7). Quoted verbatim in
|
|
28
|
+
# docs/reference/harness-adapters.md (D12); change the wording in one
|
|
29
|
+
# place only.
|
|
30
|
+
UNATTENDED_START_SENTENCE =
|
|
31
|
+
"Unattended start is delivered only where a Ruby loop owns dispatch (Codex, through runner " \
|
|
32
|
+
"watch --dispatch); on Claude Code the runner is the harness session (327 Q6), so arm /loop " \
|
|
33
|
+
"over runner watch in a live session instead."
|
|
34
|
+
|
|
35
|
+
# unattended_start?(key) -> true only for "codex" (graph.md D7): the one
|
|
36
|
+
# harness where a Ruby loop (RunnerUntilEmpty, through `runner watch
|
|
37
|
+
# --dispatch`) owns dispatch end to end with nobody on the other end
|
|
38
|
+
# making subagent calls. Claude Code IS the harness session (327 Q6), and
|
|
39
|
+
# an unknown key is never granted unattended start either - this module
|
|
40
|
+
# stays the only place that names a harness, so RunnerWatch asks this
|
|
41
|
+
# predicate and never compares a key against "codex" itself.
|
|
42
|
+
def unattended_start?(key)
|
|
43
|
+
key.to_s == "codex"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# kind -> the Claude Code agent type dispatched for it (spec Approach).
|
|
47
|
+
# Verify and research carry no `Bash` in their own frontmatter (n2); this
|
|
48
|
+
# table only names WHICH agent a kind maps to. An unknown kind gets
|
|
49
|
+
# `work`'s row, the widest of the three, never no agent type at all
|
|
50
|
+
# (matrix row 1.10), mirroring RunnerPolicy's own kind-table fallback.
|
|
51
|
+
AGENT_TYPE_BY_KIND = {
|
|
52
|
+
"work" => "plastic-node-work",
|
|
53
|
+
"verify" => "plastic-node-verify",
|
|
54
|
+
"research" => "plastic-node-research",
|
|
55
|
+
}.freeze
|
|
56
|
+
|
|
57
|
+
def agent_type_for_kind(kind)
|
|
58
|
+
AGENT_TYPE_BY_KIND.fetch(kind.to_s, AGENT_TYPE_BY_KIND["work"])
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# resolve_key(config:, override:) -> the harness key for one call (matrix
|
|
62
|
+
# rows 1.1-1.3, 1.18). `override` (the CLI's `--harness KEY`, spec D2) wins
|
|
63
|
+
# over `config`'s own `agent.type` for that one call; either source is
|
|
64
|
+
# squashed (whitespace collapsed, matrix row 1.18) before it is even
|
|
65
|
+
# compared, so a config-authored value carrying a tab or a newline can
|
|
66
|
+
# never reach `NodeLedger` unsquashed and raise `ArgumentError` mid-dispatch,
|
|
67
|
+
# after the node input is already built. A value that still is not one of
|
|
68
|
+
# KNOWN_KEYS after squashing falls back to DEFAULT_KEY with a warning
|
|
69
|
+
# (never to no adapter), so this method's return is always one of the two
|
|
70
|
+
# known, clean strings - safe to hand straight to a ledger field.
|
|
71
|
+
def resolve_key(config: {}, override: nil)
|
|
72
|
+
raw = blank?(override) ? agent_type_from_config(config) : override.to_s
|
|
73
|
+
squashed = squash(raw)
|
|
74
|
+
return squashed if KNOWN_KEYS.include?(squashed)
|
|
75
|
+
|
|
76
|
+
warn "harness_adapter: unknown harness #{raw.inspect}, falling back to #{DEFAULT_KEY.inspect}"
|
|
77
|
+
DEFAULT_KEY
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def agent_type_from_config(config)
|
|
81
|
+
return DEFAULT_KEY unless config.is_a?(Hash)
|
|
82
|
+
|
|
83
|
+
value = config.dig("agent", "type")
|
|
84
|
+
value = config.dig(:agent, :type) if blank?(value)
|
|
85
|
+
blank?(value) ? DEFAULT_KEY : value.to_s
|
|
86
|
+
end
|
|
87
|
+
private_class_method :agent_type_from_config
|
|
88
|
+
|
|
89
|
+
def squash(text)
|
|
90
|
+
text.to_s.strip.gsub(/\s+/, " ")
|
|
91
|
+
end
|
|
92
|
+
private_class_method :squash
|
|
93
|
+
|
|
94
|
+
def blank?(value)
|
|
95
|
+
value.nil? || value.to_s.strip.empty?
|
|
96
|
+
end
|
|
97
|
+
private_class_method :blank?
|
|
98
|
+
|
|
99
|
+
# render(dispatched, harness:, return_contract:) -> the rendered
|
|
100
|
+
# instruction block for `harness` (a String), or nil for an empty dispatch
|
|
101
|
+
# list (matrix row 1.14: a step that dispatched nothing prints no
|
|
102
|
+
# instruction block for a session to act on). `dispatched` is the runner's
|
|
103
|
+
# own plan entries (RunnerDispatch's `:dispatched` shape): [{node:, kind:,
|
|
104
|
+
# role:, model:, worktree:, input:}, ...]. The model and the node input path
|
|
105
|
+
# ride straight from each entry - never a fresh lookup (matrix row 1.11)
|
|
106
|
+
# and never a summary (matrix row 1.12) - and `return_contract` (the exact
|
|
107
|
+
# text RunnerDispatch::RETURN_CONTRACT already carries in the YAML plan)
|
|
108
|
+
# renders exactly once, above every node, never per node (matrix row 1.13).
|
|
109
|
+
def render(dispatched, return_contract:, harness: DEFAULT_KEY)
|
|
110
|
+
entries = Array(dispatched)
|
|
111
|
+
return nil if entries.empty?
|
|
112
|
+
|
|
113
|
+
key = KNOWN_KEYS.include?(harness.to_s) ? harness.to_s : DEFAULT_KEY
|
|
114
|
+
key == "codex" ? render_codex(entries, return_contract) : render_claude_code(entries, return_contract)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# The Claude Code rendering (spec Approach): per dispatched node, the agent
|
|
118
|
+
# type from the kind, the model the plan already resolved, and the node input
|
|
119
|
+
# path as the whole prompt.
|
|
120
|
+
def render_claude_code(entries, return_contract)
|
|
121
|
+
blocks = entries.map do |d|
|
|
122
|
+
"Dispatch #{agent_type_for_kind(d[:kind])} for #{d[:node]} (model: #{d[:model]}):\n prompt: #{d[:input]}"
|
|
123
|
+
end
|
|
124
|
+
"#{return_contract.to_s.strip}\n\n#{blocks.join("\n\n")}\n"
|
|
125
|
+
end
|
|
126
|
+
private_class_method :render_claude_code
|
|
127
|
+
|
|
128
|
+
# The Codex rendering: a Codex node runs end to end through `scripts/
|
|
129
|
+
# node-run` (n6), which reads the ledger's own `running` line directly and
|
|
130
|
+
# never through this printed block - so this names, per node, the same
|
|
131
|
+
# facts the block gives Claude Code (kind, model, node input path) framed as
|
|
132
|
+
# `node-run` calls, proving the seam renders SOMETHING for the second
|
|
133
|
+
# harness rather than nothing (matrix row 1.27), without inventing detail
|
|
134
|
+
# that belongs to n6's own adapter.
|
|
135
|
+
def render_codex(entries, return_contract)
|
|
136
|
+
blocks = entries.map do |d|
|
|
137
|
+
"node-run #{d[:node]} (#{d[:kind]}, model: #{d[:model]}):\n input: #{d[:input]}"
|
|
138
|
+
end
|
|
139
|
+
"#{return_contract.to_s.strip}\n\n#{blocks.join("\n\n")}\n"
|
|
140
|
+
end
|
|
141
|
+
private_class_method :render_codex
|
|
142
|
+
|
|
143
|
+
# cross_harness_resume(entries) -> [{node:, harnesses: [...], commits: {...}}, ...]
|
|
144
|
+
# (spec D12, 340b n8). `entries` is a node ledger's own parsed lines (the
|
|
145
|
+
# shape `NodeLedger.entries`/`entries_from_content` return): every declared
|
|
146
|
+
# node's own `harness=` values, read across every non-torn line for that
|
|
147
|
+
# subject in file order, deduplicated by first appearance (matrix row 8.6:
|
|
148
|
+
# the starting harness is whichever key showed up first). A `reclaimed`
|
|
149
|
+
# line carries no `harness=` of its own and is never a boundary here (row
|
|
150
|
+
# 8.7) - it is simply a line with nothing to contribute, scanned like any
|
|
151
|
+
# other. A node whose lines carry one key, or none at all, reports nothing
|
|
152
|
+
# (rows 8.2, 8.3): an old ledger with no `harness=` field anywhere must
|
|
153
|
+
# stay silent rather than raise.
|
|
154
|
+
#
|
|
155
|
+
# Every node named by ANY entry is scanned (row 8.8), not only the first;
|
|
156
|
+
# `RunnerAbsorb` is what put `harness=` on both the running line and the
|
|
157
|
+
# terminal line, so the commit that lands with the finishing harness's own
|
|
158
|
+
# terminal line is exactly the evidence a resumed node actually left
|
|
159
|
+
# behind (row 8.5).
|
|
160
|
+
def cross_harness_resume(entries)
|
|
161
|
+
live = Array(entries).reject { |e| e[:torn] }
|
|
162
|
+
nodes = live.map { |e| e[:subject] }.uniq
|
|
163
|
+
|
|
164
|
+
nodes.filter_map do |node|
|
|
165
|
+
node_lines = live.select { |e| e[:subject] == node }
|
|
166
|
+
keys = []
|
|
167
|
+
commits = Hash.new { |h, k| h[k] = [] }
|
|
168
|
+
|
|
169
|
+
node_lines.each do |e|
|
|
170
|
+
fields = e[:fields] || {}
|
|
171
|
+
key = fields["harness"]
|
|
172
|
+
next if blank?(key)
|
|
173
|
+
|
|
174
|
+
keys << key unless keys.include?(key)
|
|
175
|
+
commit = fields["commit"]
|
|
176
|
+
commits[key] << commit unless blank?(commit) || commits[key].include?(commit)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
next if keys.size < 2
|
|
180
|
+
|
|
181
|
+
{ node: node, harnesses: keys, commits: keys.to_h { |k| [k, commits[k]] } }
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
@@ -34,6 +34,17 @@ module HookRegistry
|
|
|
34
34
|
{ "name" => "check-update", "status" => "" },
|
|
35
35
|
] },
|
|
36
36
|
],
|
|
37
|
+
# Intent 355, n2: the call budget guard. Not one of the edit-path
|
|
38
|
+
# gates intent 302 removed (those denied a write on content; this
|
|
39
|
+
# denies a call on a per-attempt COUNT, read from the session's own
|
|
40
|
+
# transcript, never from the tool's arguments) - see scripts/hook-
|
|
41
|
+
# call-budget. Claude only: CODEX_LIVE_STATE_EVENTS below does not
|
|
42
|
+
# carry PreToolUse, so codex_hooks_json never projects it.
|
|
43
|
+
"PreToolUse" => [
|
|
44
|
+
{ "matcher" => "", "hooks" => [
|
|
45
|
+
{ "name" => "call-budget", "status" => "" },
|
|
46
|
+
] },
|
|
47
|
+
],
|
|
37
48
|
"PreCompact" => [
|
|
38
49
|
{ "matcher" => "", "hooks" => [
|
|
39
50
|
{ "name" => "savepoint", "status" => "Saving Plastic intent state..." },
|
|
@@ -68,6 +79,18 @@ module HookRegistry
|
|
|
68
79
|
{ "name" => "message-display", "status" => "" },
|
|
69
80
|
] },
|
|
70
81
|
],
|
|
82
|
+
# Continuation on Claude Code (intent 340b, G7c, n4, D5/D7): registered
|
|
83
|
+
# statically here, like every other hook, and decided at runtime by
|
|
84
|
+
# StopGate, which reads runner.stop_hook itself (default false, D9).
|
|
85
|
+
# Not one of CODEX_LIVE_STATE_EVENTS, CODEX_POST_HOOKS or
|
|
86
|
+
# CODEX_SESSION_END_HOOKS, so codex_hooks_json never picks it up and
|
|
87
|
+
# codex_hook_names stays at its pinned six (test/hook_registry_test.rb,
|
|
88
|
+
# test/codex_hooks_test.rb).
|
|
89
|
+
"Stop" => [
|
|
90
|
+
{ "matcher" => "", "hooks" => [
|
|
91
|
+
{ "name" => "stop", "status" => "" },
|
|
92
|
+
] },
|
|
93
|
+
],
|
|
71
94
|
}
|
|
72
95
|
end
|
|
73
96
|
|
|
@@ -75,8 +98,9 @@ module HookRegistry
|
|
|
75
98
|
# the PostToolUse record hook collapses from Claude's multi-tool matcher onto
|
|
76
99
|
# Codex's single apply_patch tool (181 F4: apply_patch is Codex's sole
|
|
77
100
|
# file-mutation tool; tool_name always reports apply_patch), and the live-state
|
|
78
|
-
# events project through whole.
|
|
79
|
-
#
|
|
101
|
+
# events project through whole. PreToolUse (intent 355, n2's call-budget guard,
|
|
102
|
+
# Claude only) is not one of CODEX_LIVE_STATE_EVENTS below, so Codex still
|
|
103
|
+
# carries no PreToolUse group of its own. Command invokes the
|
|
80
104
|
# codex-hook dispatcher with the hook name. Guide-settled shape [guide Part 3]:
|
|
81
105
|
# top-level {"hooks":{<Event>: [{"matcher","hooks":[{"type":"command","command",
|
|
82
106
|
# "statusMessage"}]}]}}, identical to Claude's shape, string command. Single
|
|
@@ -149,7 +173,7 @@ module HookRegistry
|
|
|
149
173
|
# name here in the SAME change. Skip it and every existing install keeps a dead
|
|
150
174
|
# registration no update will ever clean up.
|
|
151
175
|
#
|
|
152
|
-
# Never
|
|
176
|
+
# Never merge these into claude_launcher_names: that method is what doctor's
|
|
153
177
|
# hooks_exist demands be present on disk, so a retired name there makes a correct
|
|
154
178
|
# install report missing launchers.
|
|
155
179
|
RETIRED_HOOK_NAMES = %w[
|