@zalom/plastic 2.0.0-alpha.9 → 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 +11 -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 +55 -2
- 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 +153 -30
- 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 +7 -0
- 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 +23 -14
- 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 +531 -71
- package/scripts/lib/intent_screen.rb +4 -4
- 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 +350 -54
- 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 +1371 -47
- 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 +365 -28
- 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 +177 -16
- 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 +8 -3
- 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 -296
- 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 -176
- 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
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
4
|
require "fileutils"
|
|
5
|
+
require "json"
|
|
5
6
|
require_relative "screen_paint"
|
|
6
7
|
|
|
7
8
|
# MessageDisplay (intent 316a, O4/O5, round 3 concurrency fix) - the Claude
|
|
@@ -46,25 +47,87 @@ require_relative "screen_paint"
|
|
|
46
47
|
# buffered or blanked. D10 (any failure while finalizing returns the
|
|
47
48
|
# buffered original, never nil, never "") and D12 (color: false never
|
|
48
49
|
# buffers or blanks anything) are unchanged.
|
|
50
|
+
#
|
|
51
|
+
# Intent 331a: engagement is late-capable. A chunk carrying a screen opener
|
|
52
|
+
# engages the message from that chunk on, whatever its own index - not only
|
|
53
|
+
# chunk 0. Chunks before it pass through untouched (they already reached the
|
|
54
|
+
# terminal live, via the ordinary passthrough path). The engaging chunk
|
|
55
|
+
# returns the text before the opener as its displayContent and buffers the
|
|
56
|
+
# opener onward at its OWN index; SCREEN now carries that index (a decimal
|
|
57
|
+
# integer, not an empty marker) so the final chunk - a separate process in
|
|
58
|
+
# production - knows where to start waiting and splicing (D3/D6), and so
|
|
59
|
+
# NOSCREEN, no longer a final answer (D2), can be replaced once a later
|
|
60
|
+
# chunk engages. A fence line immediately wrapping the opener is dropped
|
|
61
|
+
# (D4): a lone fence in the engaging chunk's own prefix, and a lone closing
|
|
62
|
+
# fence right after the painted region in `finalize`. Neither ever reaches
|
|
63
|
+
# back into an earlier, already-displayed chunk.
|
|
64
|
+
#
|
|
65
|
+
# Intent 331a1: the decision marker (D1-D3). 331a's own comment above already
|
|
66
|
+
# names the concurrency; what it did not close is chunk 0's own boot time.
|
|
67
|
+
# Chunk 0's Ruby process takes on the order of 150 ms to boot before it ever
|
|
68
|
+
# writes SCREEN or NOSCREEN - long enough, under a fast real stream, for a
|
|
69
|
+
# dozen or more later chunks to be judged with nothing on disk at all, so
|
|
70
|
+
# every one of them fell back to the cheap shape test and, being ordinary
|
|
71
|
+
# non-table prose, passed straight through plain. The bash launcher (hooks/
|
|
72
|
+
# message-display) now stakes a PENDING file with builtins the instant
|
|
73
|
+
# chunk 0 is handed off, before Ruby ever starts, so a later chunk finds the
|
|
74
|
+
# message directory within microseconds instead of after Ruby's own boot.
|
|
75
|
+
# While PENDING exists, a later chunk polls for the real decision WHATEVER
|
|
76
|
+
# ITS OWN SHAPE looks like - `maybe_screen?` is not consulted at all, because
|
|
77
|
+
# a decision is certainly coming, and the cheap shape gate exists only for
|
|
78
|
+
# the "nothing at all exists yet, is a wait even worth paying for" case,
|
|
79
|
+
# which no longer applies once something IS on disk. A PENDING whose mtime
|
|
80
|
+
# is already older than THIS chunk's own poll budget reads as NOSCREEN (D2,
|
|
81
|
+
# fail open): chunk 0 must have died or hung, and waiting out a whole budget
|
|
82
|
+
# for a decision that is provably not coming would only delay every chunk
|
|
83
|
+
# behind it. That staleness check runs ONCE, before any polling, since a
|
|
84
|
+
# file's mtime never changes while this process looks at it. The poll
|
|
85
|
+
# budget itself scales with the chunk's own index (`budget_ms`, D3): base
|
|
86
|
+
# wait_ms plus index_wait_ms per index, capped at max_wait_ms, so the final
|
|
87
|
+
# chunk of a long streamed message (335 chunks, in the live capture that
|
|
88
|
+
# reproduced this) is allowed to wait for a decision that is certainly on
|
|
89
|
+
# its way, while chunk 1 of an ordinary short message still fails open
|
|
90
|
+
# quickly. Chunk 0 removes PENDING the moment it writes SCREEN or NOSCREEN
|
|
91
|
+
# (`write_screen`/`write_noscreen`), on both paths, so "a decision already
|
|
92
|
+
# exists" and "PENDING is still there" are never both true for long.
|
|
49
93
|
class MessageDisplay
|
|
50
94
|
# 317a (A4): engagement is grammar, not identity - any screen-family
|
|
51
95
|
# opener engages, with NO intent-id resolution (the roster and delay
|
|
52
|
-
# screens have none to resolve). ScreenPaint owns the full grammar.
|
|
96
|
+
# screens have none to resolve). ScreenPaint owns the full grammar. Used
|
|
97
|
+
# per LINE (331a), not only against the start of a whole delta: a chunk's
|
|
98
|
+
# own text is scanned line by line for the first line that opens a screen,
|
|
99
|
+
# wherever it falls.
|
|
53
100
|
ENGAGE_RE = /\A(?:##? )?[▶✔] /.freeze
|
|
101
|
+
# 331a (D4): a lone fence line, opening (optional info string) or closing
|
|
102
|
+
# (never one), by itself on its own line.
|
|
103
|
+
FENCE_OPEN_RE = /\A```[^\n]*\z/.freeze
|
|
104
|
+
FENCE_CLOSE_RE = /\A```\z/.freeze
|
|
54
105
|
BUFFER_DIR_NAME = "plastic-message-display"
|
|
55
106
|
BUFFER_MAX_AGE_SECONDS = 3600
|
|
56
107
|
SCREEN_FILE = "SCREEN"
|
|
57
108
|
NOSCREEN_FILE = "NOSCREEN"
|
|
58
|
-
|
|
109
|
+
# 331a1 (D1): staked by the bash launcher, with builtins, the instant
|
|
110
|
+
# chunk 0 is handed off - before Ruby ever boots. Replaced by SCREEN or
|
|
111
|
+
# NOSCREEN (D2), never read by this class as a decision in its own right.
|
|
112
|
+
PENDING_FILE = "PENDING"
|
|
113
|
+
|
|
114
|
+
# `trace` (331a1): an optional callable taking one Hash per chunk. The class
|
|
115
|
+
# stays pure - it never reads PLASTIC_HOOK_TRACE, never opens a file of its
|
|
116
|
+
# own; the CLI reads the variable and injects `file_trace`. nil, the
|
|
117
|
+
# default, costs the common path one `unless` and nothing else.
|
|
59
118
|
def initialize(tmp_root:, plastic_home:, color:, now:, wait_ms: 300, poll_ms: 20,
|
|
60
|
-
|
|
119
|
+
index_wait_ms: 20, max_wait_ms: 2000,
|
|
120
|
+
sleeper: ->(seconds) { sleep(seconds) }, trace: nil)
|
|
61
121
|
@tmp_root = tmp_root
|
|
62
122
|
@plastic_home = plastic_home
|
|
63
123
|
@color = color
|
|
64
124
|
@now = now
|
|
65
125
|
@wait_ms = wait_ms
|
|
66
126
|
@poll_ms = poll_ms
|
|
127
|
+
@index_wait_ms = index_wait_ms
|
|
128
|
+
@max_wait_ms = max_wait_ms
|
|
67
129
|
@sleeper = sleeper
|
|
130
|
+
@trace = trace
|
|
68
131
|
end
|
|
69
132
|
|
|
70
133
|
def handle(payload)
|
|
@@ -84,11 +147,28 @@ class MessageDisplay
|
|
|
84
147
|
|
|
85
148
|
dir = self.class.buffer_path(tmp_root: @tmp_root, session_id: session_id, message_id: message_id)
|
|
86
149
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
150
|
+
@trace_detail = {}
|
|
151
|
+
result =
|
|
152
|
+
if index == 0
|
|
153
|
+
handle_chunk_zero(dir, delta, cwd, final)
|
|
154
|
+
else
|
|
155
|
+
handle_later_chunk(dir, index, delta, final)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
emit_trace(index, final, result)
|
|
159
|
+
result
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# One row per chunk, only when a sink was injected. Never raises: a trace
|
|
163
|
+
# is a diagnostic and must not be able to change what the hook returns.
|
|
164
|
+
def emit_trace(index, final, result)
|
|
165
|
+
return unless @trace
|
|
166
|
+
|
|
167
|
+
row = { "index" => index, "final" => final,
|
|
168
|
+
"displayed_bytes" => result.is_a?(String) ? result.bytesize : nil }
|
|
169
|
+
@trace.call(row.merge(@trace_detail.to_h))
|
|
170
|
+
rescue StandardError
|
|
171
|
+
nil
|
|
92
172
|
end
|
|
93
173
|
|
|
94
174
|
# The message directory both this class and the bash launcher (hooks/
|
|
@@ -107,35 +187,73 @@ class MessageDisplay
|
|
|
107
187
|
File.join(buffer_path(tmp_root: tmp_root, session_id: session_id, message_id: message_id), SCREEN_FILE)
|
|
108
188
|
end
|
|
109
189
|
|
|
190
|
+
# A trace sink that appends one JSON object per chunk to `path`. Every
|
|
191
|
+
# failure is swallowed: an unwritable path must never turn a diagnostic
|
|
192
|
+
# into a broken hook (D5, fail open).
|
|
193
|
+
def self.file_trace(path)
|
|
194
|
+
lambda do |row|
|
|
195
|
+
File.open(path, "a") { |f| f.puts(JSON.generate(row)) }
|
|
196
|
+
rescue StandardError
|
|
197
|
+
nil
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
110
201
|
def self.noscreen_path(tmp_root:, session_id:, message_id:)
|
|
111
202
|
File.join(buffer_path(tmp_root: tmp_root, session_id: session_id, message_id: message_id), NOSCREEN_FILE)
|
|
112
203
|
end
|
|
113
204
|
|
|
205
|
+
# 331a1 (matrix L1): the bash launcher (hooks/message-display) and this
|
|
206
|
+
# class must agree, byte for byte, on where PENDING lives - the same
|
|
207
|
+
# contract `buffer_path` already carries for SCREEN/NOSCREEN (matrix 40).
|
|
208
|
+
def self.pending_path(tmp_root:, session_id:, message_id:)
|
|
209
|
+
File.join(buffer_path(tmp_root: tmp_root, session_id: session_id, message_id: message_id), PENDING_FILE)
|
|
210
|
+
end
|
|
211
|
+
|
|
114
212
|
private
|
|
115
213
|
|
|
116
214
|
# Chunk 0 decides, synchronously, before anything else touches this
|
|
117
|
-
# message:
|
|
118
|
-
#
|
|
119
|
-
#
|
|
120
|
-
#
|
|
215
|
+
# message: does ITS OWN delta carry an opener anywhere (331a; used to be
|
|
216
|
+
# only at the very start)? No opener writes NOSCREEN so every later chunk
|
|
217
|
+
# can decide instantly rather than waiting out its own budget for a
|
|
218
|
+
# decision that will never arrive - but NOSCREEN is no longer final (D2):
|
|
219
|
+
# a later chunk carrying an opener still replaces it.
|
|
121
220
|
def handle_chunk_zero(dir, delta, _cwd, final)
|
|
122
|
-
|
|
123
|
-
unless
|
|
221
|
+
split = split_at_opener(delta)
|
|
222
|
+
unless split
|
|
223
|
+
@trace_detail["decision"] = "noscreen"
|
|
124
224
|
write_noscreen(dir)
|
|
125
225
|
return nil
|
|
126
226
|
end
|
|
127
227
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
final ? finalize_final(dir, 0) : ""
|
|
228
|
+
@trace_detail["decision"] = "engage"
|
|
229
|
+
engage(dir, 0, split, final)
|
|
131
230
|
end
|
|
132
231
|
|
|
133
|
-
# A later chunk (index > 0)
|
|
134
|
-
#
|
|
135
|
-
#
|
|
136
|
-
#
|
|
232
|
+
# A later chunk (index > 0) tests its OWN delta for an opener FIRST,
|
|
233
|
+
# before consulting any existing decision (331a, D2): an opener engages
|
|
234
|
+
# the message whatever the current decision says, including when NOSCREEN
|
|
235
|
+
# is already on disk. Only once its own delta carries no opener does it
|
|
236
|
+
# fall back to the original decision-driven wait.
|
|
137
237
|
def handle_later_chunk(dir, index, delta, final)
|
|
138
|
-
|
|
238
|
+
# 331a1: an opener engages only a message that is NOT already a screen.
|
|
239
|
+
# 331a's late engagement exists for the prose-first reply, where chunk 0
|
|
240
|
+
# wrote NOSCREEN and a later chunk carries the title; it must not fire
|
|
241
|
+
# again once SCREEN is on disk. A reply can hold several screens back to
|
|
242
|
+
# back - the roster is a table then ten cards, the session report is many
|
|
243
|
+
# delivered screens - and re-engaging on each one returned that chunk's
|
|
244
|
+
# own prefix as raw Markdown and rewrote the start index, so the final
|
|
245
|
+
# chunk spliced from the LAST opener and everything before it reached the
|
|
246
|
+
# terminal unpainted. Inside an engaged message a later opener is simply
|
|
247
|
+
# content: it is buffered like any other line and the painter, which
|
|
248
|
+
# already understands a run of screens, lays all of them out.
|
|
249
|
+
split = split_at_opener(delta)
|
|
250
|
+
if split && !engaged?(dir)
|
|
251
|
+
@trace_detail["decision"] = "engage"
|
|
252
|
+
return engage(dir, index, split, final)
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
decision = wait_for_decision(dir, gate_delta: final ? nil : delta, index: index)
|
|
256
|
+
@trace_detail["decision"] = decision.to_s
|
|
139
257
|
|
|
140
258
|
return nil unless decision == :screen
|
|
141
259
|
|
|
@@ -143,17 +261,83 @@ class MessageDisplay
|
|
|
143
261
|
final ? finalize_final(dir, index) : ""
|
|
144
262
|
end
|
|
145
263
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
264
|
+
def engaged?(dir)
|
|
265
|
+
File.exist?(File.join(dir, SCREEN_FILE))
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# Engages the message starting at THIS chunk (whatever its index): writes
|
|
269
|
+
# SCREEN carrying this chunk's index (replacing any NOSCREEN, D2/D6),
|
|
270
|
+
# buffers the opener onward at this chunk's own index, and returns the
|
|
271
|
+
# text before the opener (fence-stripped, D4) as the displayContent. When
|
|
272
|
+
# this chunk is also final, the prefix is prepended to whatever `finalize`
|
|
273
|
+
# produces (painted, or the buffered original on a fail-open) rather than
|
|
274
|
+
# dropped.
|
|
275
|
+
def engage(dir, index, split, final)
|
|
276
|
+
prefix, rest = split
|
|
277
|
+
prefix = strip_preceding_fence(prefix)
|
|
278
|
+
write_screen(dir, index)
|
|
279
|
+
remove_noscreen(dir)
|
|
280
|
+
write_chunk(dir, index, rest)
|
|
281
|
+
|
|
282
|
+
return prefix unless final
|
|
283
|
+
|
|
284
|
+
finalized = finalize_final(dir, index)
|
|
285
|
+
prefix.empty? ? finalized : "#{prefix}#{finalized}"
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
# Scans `delta` line by line for the first line that opens a screen
|
|
289
|
+
# (331a: an opener can fall anywhere in a chunk's own delta, not only at
|
|
290
|
+
# its start). Returns [prefix, rest] - the text before that line, and the
|
|
291
|
+
# line onward - or nil when no line in this delta engages.
|
|
292
|
+
def split_at_opener(delta)
|
|
293
|
+
lines = delta.each_line.to_a
|
|
294
|
+
idx = lines.index { |line| ENGAGE_RE.match?(line.sub(/\A[ \t]+/, "")) }
|
|
295
|
+
return nil unless idx
|
|
296
|
+
|
|
297
|
+
[lines[0...idx].join, lines[idx..].join]
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
# 331a (D4): a lone fence line immediately preceding the opener, inside
|
|
301
|
+
# THIS SAME chunk's own prefix, is dropped - it never reaches the screen
|
|
302
|
+
# (it would otherwise print directly above the painted block). A fence in
|
|
303
|
+
# an earlier chunk is never touched: it was already displayed, verbatim,
|
|
304
|
+
# by that earlier chunk's own return value.
|
|
305
|
+
def strip_preceding_fence(prefix)
|
|
306
|
+
lines = prefix.each_line.to_a
|
|
307
|
+
return prefix if lines.empty?
|
|
308
|
+
return prefix unless FENCE_OPEN_RE.match?(lines.last.strip)
|
|
309
|
+
|
|
310
|
+
lines[0...-1].join
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
# Checks for an existing decision first (free). Then, 331a1 (D1/D2), the
|
|
314
|
+
# whole fix: when PENDING exists, a decision is certainly coming, so this
|
|
315
|
+
# chunk polls for it WHATEVER ITS OWN SHAPE looks like - `maybe_screen?`
|
|
316
|
+
# is never even consulted on this branch - unless PENDING is already
|
|
317
|
+
# stale (older than this chunk's own budget), which reads as NOSCREEN at
|
|
318
|
+
# once, fail open, without ever polling. Only when there is no PENDING
|
|
319
|
+
# AT ALL (chunk 0 has not even been handed off to the bash launcher yet,
|
|
320
|
+
# or this replay never wrote one) does today's original behavior apply:
|
|
321
|
+
# the cheap shape test gates whether a bounded poll is worth paying for.
|
|
322
|
+
# `gate_delta: nil` (the final chunk) skips that shape test entirely and
|
|
323
|
+
# always polls.
|
|
324
|
+
def wait_for_decision(dir, gate_delta:, index:)
|
|
151
325
|
decision = read_decision_now(dir)
|
|
152
326
|
return decision if decision
|
|
153
327
|
|
|
328
|
+
if pending?(dir)
|
|
329
|
+
return :noscreen if pending_stale?(dir, index)
|
|
330
|
+
|
|
331
|
+
return poll_for_decision(dir, index)
|
|
332
|
+
end
|
|
333
|
+
|
|
154
334
|
return :timeout if gate_delta && !maybe_screen?(gate_delta)
|
|
155
335
|
|
|
156
|
-
|
|
336
|
+
poll_for_decision(dir, index)
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def poll_for_decision(dir, index)
|
|
340
|
+
max_polls_for_budget(index).times do
|
|
157
341
|
@sleeper.call(@poll_ms / 1000.0)
|
|
158
342
|
decision = read_decision_now(dir)
|
|
159
343
|
return decision if decision
|
|
@@ -169,6 +353,38 @@ class MessageDisplay
|
|
|
169
353
|
nil
|
|
170
354
|
end
|
|
171
355
|
|
|
356
|
+
def pending?(dir)
|
|
357
|
+
File.exist?(File.join(dir, PENDING_FILE))
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
# Checked ONCE, before any polling - a file's mtime never changes while
|
|
361
|
+
# this process is looking at it, so re-checking inside the poll loop
|
|
362
|
+
# would only ever repeat the same answer. Any error reading the mtime
|
|
363
|
+
# (a race where PENDING vanished between `pending?` and here, most
|
|
364
|
+
# likely because the real decision just landed) is NOT staleness: it
|
|
365
|
+
# falls through to the ordinary poll, which will pick up that decision
|
|
366
|
+
# on its very next read.
|
|
367
|
+
def pending_stale?(dir, index)
|
|
368
|
+
age_ms = (@now.to_f - File.mtime(File.join(dir, PENDING_FILE)).to_f) * 1000
|
|
369
|
+
age_ms > budget_ms(index)
|
|
370
|
+
rescue StandardError
|
|
371
|
+
false
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
# 331a (M5a): the start index crosses process boundaries through SCREEN's
|
|
375
|
+
# own content, never in-memory state - the final chunk is routinely a
|
|
376
|
+
# SEPARATE process from the one that engaged. An empty or missing file
|
|
377
|
+
# reads back as 0 (chunk 0 engaged, today's shape, so nothing that ever
|
|
378
|
+
# wrote an empty SCREEN breaks).
|
|
379
|
+
def read_start_index(dir)
|
|
380
|
+
path = File.join(dir, SCREEN_FILE)
|
|
381
|
+
return 0 unless File.exist?(path)
|
|
382
|
+
|
|
383
|
+
File.read(path).to_i
|
|
384
|
+
rescue StandardError
|
|
385
|
+
0
|
|
386
|
+
end
|
|
387
|
+
|
|
172
388
|
# Cheap, local, no file I/O: could this chunk's own delta plausibly be
|
|
173
389
|
# part of an intent screen (ignoring leading whitespace)? Every chunk of
|
|
174
390
|
# every ordinary prose message answers no, at zero cost.
|
|
@@ -177,46 +393,70 @@ class MessageDisplay
|
|
|
177
393
|
stripped.empty? || stripped.start_with?("|") || stripped.start_with?("**")
|
|
178
394
|
end
|
|
179
395
|
|
|
180
|
-
# The final chunk additionally waits (same budget) for every
|
|
181
|
-
#
|
|
182
|
-
#
|
|
183
|
-
#
|
|
396
|
+
# The final chunk additionally waits (same budget) for every chunk file
|
|
397
|
+
# from the start index onward to exist before it reassembles and splices
|
|
398
|
+
# (331a, D3/M5: from the start index, not from 0 - chunks before the
|
|
399
|
+
# engaging one were never buffered at all, so waiting for them would only
|
|
400
|
+
# ever burn the whole budget for files that will never appear). On
|
|
401
|
+
# timeout it proceeds anyway with whatever is there (matrix, lead's
|
|
402
|
+
# guard): never nil, never swallowed.
|
|
184
403
|
def finalize_final(dir, index)
|
|
185
|
-
|
|
404
|
+
start_index = read_start_index(dir)
|
|
405
|
+
wait_for_chunk_files(dir, start_index, index)
|
|
186
406
|
|
|
187
407
|
buffered = nil
|
|
188
408
|
begin
|
|
189
|
-
buffered = read_buffered_chunks(dir, index)
|
|
409
|
+
buffered = read_buffered_chunks(dir, start_index, index)
|
|
190
410
|
finalize(buffered, nil)
|
|
191
411
|
rescue StandardError
|
|
192
|
-
buffered
|
|
412
|
+
# A read failing inside the assignment above leaves `buffered` at its
|
|
413
|
+
# nil default (the assignment never completes), which the review pass
|
|
414
|
+
# caught: D8/D10 promise the buffered original on any finalize
|
|
415
|
+
# failure, never nil, once chunks were blanked. `||=` covers exactly
|
|
416
|
+
# that gap without touching the ordinary case (buffered already holds
|
|
417
|
+
# the real chunks read before `finalize` itself raised).
|
|
418
|
+
buffered ||= ""
|
|
193
419
|
ensure
|
|
194
420
|
FileUtils.rm_rf(dir)
|
|
195
421
|
end
|
|
196
422
|
end
|
|
197
423
|
|
|
198
|
-
|
|
199
|
-
|
|
424
|
+
# 331a1 (D3): index-scaled too, same as the decision poll - the final
|
|
425
|
+
# chunk of a long streamed message (335 chunks, in the live capture that
|
|
426
|
+
# reproduced this) must be allowed to wait long enough for the earlier
|
|
427
|
+
# chunk files to land, not just the base wait_ms an ordinary short
|
|
428
|
+
# message gets by with.
|
|
429
|
+
def wait_for_chunk_files(dir, start_index, index)
|
|
430
|
+
return if index <= start_index
|
|
200
431
|
|
|
201
|
-
needed = (
|
|
202
|
-
max_polls_for_budget.times do
|
|
432
|
+
needed = (start_index...index).map(&:to_s)
|
|
433
|
+
max_polls_for_budget(index).times do
|
|
203
434
|
return if needed.all? { |n| File.exist?(File.join(dir, n)) }
|
|
204
435
|
|
|
205
436
|
@sleeper.call(@poll_ms / 1000.0)
|
|
206
437
|
end
|
|
207
438
|
end
|
|
208
439
|
|
|
209
|
-
|
|
440
|
+
# 331a1 (D3): base wait_ms plus index_wait_ms per chunk index, capped at
|
|
441
|
+
# max_wait_ms - a chunk deep into a long streamed message is certainly
|
|
442
|
+
# going to see its decision eventually, so it is allowed to wait longer
|
|
443
|
+
# than chunk 1 of an ordinary short message.
|
|
444
|
+
def budget_ms(index)
|
|
445
|
+
[@wait_ms + @index_wait_ms * index.to_i, @max_wait_ms].min
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
def max_polls_for_budget(index)
|
|
210
449
|
return 0 unless @poll_ms.to_f.positive?
|
|
211
450
|
|
|
212
|
-
(
|
|
451
|
+
(budget_ms(index) / @poll_ms.to_f).ceil
|
|
213
452
|
end
|
|
214
453
|
|
|
215
|
-
# Whatever chunk files exist, in index order,
|
|
216
|
-
# chunk that never arrived, or arrived too late)
|
|
217
|
-
# blocking reassembly (lead's guard: never return
|
|
218
|
-
|
|
219
|
-
|
|
454
|
+
# Whatever chunk files exist FROM THE START INDEX onward, in index order,
|
|
455
|
+
# concatenated -- gaps (a chunk that never arrived, or arrived too late)
|
|
456
|
+
# are skipped rather than blocking reassembly (lead's guard: never return
|
|
457
|
+
# nothing).
|
|
458
|
+
def read_buffered_chunks(dir, start_index, index)
|
|
459
|
+
(start_index..index).filter_map do |i|
|
|
220
460
|
path = File.join(dir, i.to_s)
|
|
221
461
|
File.exist?(path) ? File.read(path) : nil
|
|
222
462
|
end.join
|
|
@@ -239,29 +479,85 @@ class MessageDisplay
|
|
|
239
479
|
start = lines.index { |l| ScreenPaint.classify(l) == :opener }
|
|
240
480
|
return buffered unless start
|
|
241
481
|
|
|
242
|
-
|
|
243
|
-
painted = ScreenPaint.paint(lines[start...
|
|
482
|
+
region_stop = ScreenPaint.region_end(lines, start)
|
|
483
|
+
painted = ScreenPaint.paint(lines[start...region_stop].join, color: true, markdown_safe: true)
|
|
484
|
+
trace_region(buffered, lines, start, region_stop, painted)
|
|
244
485
|
return buffered unless painted
|
|
245
486
|
|
|
246
|
-
|
|
487
|
+
# 331a (D4): a lone CLOSING fence immediately after the painted region is
|
|
488
|
+
# dropped - never the painting boundary itself (region_stop, used above,
|
|
489
|
+
# is untouched), only where the suffix starts. An unrelated fenced code
|
|
490
|
+
# block further down, with prose or a blank line between it and the
|
|
491
|
+
# region, is never adjacent, so it always survives verbatim (M8b); a
|
|
492
|
+
# closing fence never carries an info string, so an adjacent OPENING
|
|
493
|
+
# fence of a real code block (which usually does) is never mistaken for
|
|
494
|
+
# it either.
|
|
495
|
+
suffix_start = region_stop
|
|
496
|
+
suffix_start += 1 if suffix_start < lines.length && FENCE_CLOSE_RE.match?(lines[suffix_start].strip)
|
|
497
|
+
|
|
498
|
+
suffix = lines[suffix_start..].to_a.join.sub(/\A\n+/, "")
|
|
247
499
|
out = +"#{lines[0...start].join}#{painted.rstrip}\n"
|
|
248
500
|
out << "\n#{suffix}" unless suffix.empty?
|
|
249
501
|
out
|
|
250
502
|
end
|
|
251
503
|
|
|
504
|
+
# What the final chunk actually painted, for the opt-in trace: how much was
|
|
505
|
+
# buffered, where the region ran, and the first line the grammar rejected -
|
|
506
|
+
# the one fact a terminal capture cannot give you. Computed only when a
|
|
507
|
+
# sink was injected, so an ordinary run pays nothing.
|
|
508
|
+
def trace_region(buffered, lines, start, region_stop, painted)
|
|
509
|
+
return unless @trace
|
|
510
|
+
|
|
511
|
+
rejected = ScreenPaint.first_rejected(lines, start)
|
|
512
|
+
@trace_detail.merge!(
|
|
513
|
+
"buffered_bytes" => buffered.bytesize,
|
|
514
|
+
"buffered_lines" => lines.length,
|
|
515
|
+
"region_start" => start,
|
|
516
|
+
"region_stop" => region_stop,
|
|
517
|
+
"painted" => !painted.nil?,
|
|
518
|
+
"first_rejected_line" => rejected && { "index" => rejected[0], "text" => rejected[1][0, 200] },
|
|
519
|
+
)
|
|
520
|
+
rescue StandardError
|
|
521
|
+
nil
|
|
522
|
+
end
|
|
523
|
+
|
|
252
524
|
def write_chunk(dir, index, delta)
|
|
253
525
|
atomic_write(File.join(dir, index.to_s), delta)
|
|
254
526
|
end
|
|
255
527
|
|
|
256
|
-
#
|
|
257
|
-
#
|
|
258
|
-
#
|
|
259
|
-
|
|
260
|
-
|
|
528
|
+
# SCREEN's content is the engaging chunk's own index, as a decimal integer
|
|
529
|
+
# (331a, D6) - read back by `read_start_index` so the final chunk (a
|
|
530
|
+
# separate process, in production) knows where to start waiting and
|
|
531
|
+
# splicing, and so `finalize_final` never touches chunks that were passed
|
|
532
|
+
# through untouched before engagement.
|
|
533
|
+
# 331a1 (D2): the decision REPLACES PENDING, on this path too, whichever
|
|
534
|
+
# chunk turns out to be the one that engages.
|
|
535
|
+
def write_screen(dir, index)
|
|
536
|
+
atomic_write(File.join(dir, SCREEN_FILE), "#{index}\n")
|
|
537
|
+
remove_pending(dir)
|
|
261
538
|
end
|
|
262
539
|
|
|
540
|
+
# 331a1 (D2): same replacement on the NOSCREEN path, so a later chunk
|
|
541
|
+
# never finds both PENDING and NOSCREEN and has to choose between them.
|
|
263
542
|
def write_noscreen(dir)
|
|
264
543
|
atomic_write(File.join(dir, NOSCREEN_FILE), "")
|
|
544
|
+
remove_pending(dir)
|
|
545
|
+
end
|
|
546
|
+
|
|
547
|
+
# 331a (D2/D6): NOSCREEN is no longer a final answer - a later chunk that
|
|
548
|
+
# engages replaces it with SCREEN and, for safety, removes NOSCREEN so a
|
|
549
|
+
# stale marker can never be read back once the real decision exists.
|
|
550
|
+
def remove_noscreen(dir)
|
|
551
|
+
FileUtils.rm_f(File.join(dir, NOSCREEN_FILE))
|
|
552
|
+
end
|
|
553
|
+
|
|
554
|
+
# 331a1 (D2): removal must never raise - a decision was already written
|
|
555
|
+
# successfully by the time this runs, and a stray filesystem error here
|
|
556
|
+
# must never turn a successful decision into an unhandled exception.
|
|
557
|
+
def remove_pending(dir)
|
|
558
|
+
FileUtils.rm_f(File.join(dir, PENDING_FILE))
|
|
559
|
+
rescue StandardError
|
|
560
|
+
nil
|
|
265
561
|
end
|
|
266
562
|
|
|
267
563
|
def atomic_write(path, content)
|