@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
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "digest"
|
|
5
|
+
|
|
6
|
+
# DataBoundary (intent 338, G5, n1): the trust boundary a node input is
|
|
7
|
+
# built over. A data block carries text other agents and the owner wrote,
|
|
8
|
+
# and is opened by `<<<PLASTIC-DATA:<token> label="..." source="...">>>` and
|
|
9
|
+
# closed by `<<<END-PLASTIC-DATA:<token>>>`. The token is content-derived
|
|
10
|
+
# (spec D4), so the same payloads always produce the same token and the
|
|
11
|
+
# node input stays deterministic; the escaping rule (spec D5) runs on every
|
|
12
|
+
# payload independently of the token, so a payload carrying this node input's
|
|
13
|
+
# own closing marker still cannot close its block. Attribute values
|
|
14
|
+
# (`label`, `source`) are sanitized separately, by a whitelist (spec D5a),
|
|
15
|
+
# because they are not payload text and the payload escaping rule does not
|
|
16
|
+
# cover them.
|
|
17
|
+
#
|
|
18
|
+
# Pure and side-effect-free: no I/O, no clock, nothing raised across the
|
|
19
|
+
# boundary. `estimate_tokens` is the one arithmetic every budget in this
|
|
20
|
+
# delivery is spent in (spec D6): bytes over four, rounded.
|
|
21
|
+
module DataBoundary
|
|
22
|
+
module_function
|
|
23
|
+
|
|
24
|
+
MARKER_OPEN = "<<<PLASTIC-DATA:"
|
|
25
|
+
MARKER_CLOSE = "<<<END-PLASTIC-DATA:"
|
|
26
|
+
ATTR_MAX = 200
|
|
27
|
+
|
|
28
|
+
# The attribute whitelist (spec D5a): anything outside this class becomes
|
|
29
|
+
# `_`. A quote, an angle bracket, a backslash, a newline and a carriage
|
|
30
|
+
# return are therefore all impossible in a marker line.
|
|
31
|
+
ATTR_SAFE_CHARS_RE = %r{[^A-Za-z0-9 _.,:#/@+=-]}.freeze
|
|
32
|
+
|
|
33
|
+
OPEN_LINE_RE = /\A#{Regexp.escape(MARKER_OPEN)}([0-9a-f]+) label="([^"]*)" source="([^"]*)">>>\z/.freeze
|
|
34
|
+
CLOSE_LINE_RE = /\A#{Regexp.escape(MARKER_CLOSE)}([0-9a-f]+)>>>\z/.freeze
|
|
35
|
+
|
|
36
|
+
# The first twelve hex characters of the SHA-256 over the payloads joined
|
|
37
|
+
# by a newline (spec D4): fixed per node input, unguessable from any single
|
|
38
|
+
# record file, deterministic across two builds of the same payloads.
|
|
39
|
+
def boundary_token(payloads)
|
|
40
|
+
Digest::SHA256.hexdigest(Array(payloads).map(&:to_s).join("\n"))[0, 12]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# One-way marker escaping (spec D5), plus a UTF-8 scrub (matrix 1.10) so
|
|
44
|
+
# one invalid byte anywhere in a record never raises the whole node input
|
|
45
|
+
# build. Runs regardless of what token trails the marker text, because a
|
|
46
|
+
# payload copied from an earlier node input may carry ANY token, not only this
|
|
47
|
+
# one (matrix 1.2's concern applied to escaping rather than to the token
|
|
48
|
+
# itself).
|
|
49
|
+
def escape(payload)
|
|
50
|
+
text = payload.to_s.scrub
|
|
51
|
+
text = text.gsub(MARKER_OPEN) { "<<<\\PLASTIC-DATA:" }
|
|
52
|
+
text.gsub(MARKER_CLOSE) { "<<<\\END-PLASTIC-DATA:" }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Whitelist-sanitize and truncate a marker attribute value (spec D5a).
|
|
56
|
+
# Every character outside the whitelist becomes `_`; the result is
|
|
57
|
+
# truncated to ATTR_MAX characters, so neither a hostile value nor a
|
|
58
|
+
# multi-kilobyte one can escape or bloat a marker line.
|
|
59
|
+
def attr_safe(value)
|
|
60
|
+
value.to_s.scrub.gsub(ATTR_SAFE_CHARS_RE, "_")[0, ATTR_MAX]
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# One complete, closed data block ending in a newline. The payload is
|
|
64
|
+
# escaped and scrubbed first; both markers sit alone on their own line
|
|
65
|
+
# (matrix 1.4), even when the payload is empty (matrix 1.9).
|
|
66
|
+
def wrap(payload, label:, source:, token:)
|
|
67
|
+
body = escape(payload)
|
|
68
|
+
body += "\n" unless body.empty? || body.end_with?("\n")
|
|
69
|
+
open_line = "#{MARKER_OPEN}#{token} label=\"#{attr_safe(label)}\" source=\"#{attr_safe(source)}\">>>\n"
|
|
70
|
+
close_line = "#{MARKER_CLOSE}#{token}>>>\n"
|
|
71
|
+
"#{open_line}#{body}#{close_line}"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# [{label:, source:, payload:}], in order. Matches markers case-sensitively
|
|
75
|
+
# (matrix 1.16: no `/i`, ever) and closes a block only on a close marker
|
|
76
|
+
# whose token matches the block's own opening token, so a stray marker for
|
|
77
|
+
# a different token found mid-payload (which the escaping rule already
|
|
78
|
+
# makes unreachable from real payload text) is never mistaken for this
|
|
79
|
+
# block's close.
|
|
80
|
+
def unwrap(text)
|
|
81
|
+
lines = text.to_s.each_line.to_a
|
|
82
|
+
blocks = []
|
|
83
|
+
i = 0
|
|
84
|
+
while i < lines.length
|
|
85
|
+
m = lines[i].chomp("\n").match(OPEN_LINE_RE)
|
|
86
|
+
unless m
|
|
87
|
+
i += 1
|
|
88
|
+
next
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
token, label, source = m[1], m[2], m[3]
|
|
92
|
+
payload_lines = []
|
|
93
|
+
i += 1
|
|
94
|
+
while i < lines.length
|
|
95
|
+
cm = lines[i].chomp("\n").match(CLOSE_LINE_RE)
|
|
96
|
+
break if cm && cm[1] == token
|
|
97
|
+
|
|
98
|
+
payload_lines << lines[i]
|
|
99
|
+
i += 1
|
|
100
|
+
end
|
|
101
|
+
blocks << { label: label, source: source, payload: payload_lines.join }
|
|
102
|
+
i += 1
|
|
103
|
+
end
|
|
104
|
+
blocks
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Disarm any line that is, on its own, a complete open or close marker
|
|
108
|
+
# (intent 338 post-execution review, finding A2). Blocks 1 and 5 are
|
|
109
|
+
# instruction, raw-interpolated rather than wrapped, on the grounds that
|
|
110
|
+
# they are orchestrator- and project-record-authored; that trust does not
|
|
111
|
+
# extend to `release.verify` in project.yml, which the owner and other
|
|
112
|
+
# agents write. `escape` already disarms every occurrence of the marker
|
|
113
|
+
# substring wherever it falls, but a marker sitting mid-line (matrix 1.5's
|
|
114
|
+
# convention of quoting a matrix row verbatim inside a node file) is
|
|
115
|
+
# already harmless, because `unwrap` only recognizes a marker that is the
|
|
116
|
+
# WHOLE line. Rewriting a harmless mid-line quote would be needless churn
|
|
117
|
+
# on instruction text an owner reads, so only a line that fully matches
|
|
118
|
+
# `OPEN_LINE_RE` or `CLOSE_LINE_RE` gets `escape`'s backslash treatment.
|
|
119
|
+
def neutralize_marker_lines(text)
|
|
120
|
+
text.to_s.each_line.map do |line|
|
|
121
|
+
chomped = line.chomp("\n")
|
|
122
|
+
chomped.match?(OPEN_LINE_RE) || chomped.match?(CLOSE_LINE_RE) ? escape(line) : line
|
|
123
|
+
end.join
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# (bytes / 4.0).round (spec D6): the estimate G10 measures against. Taken
|
|
127
|
+
# over bytesize, never characters, so multi-byte text is never
|
|
128
|
+
# under-counted.
|
|
129
|
+
def estimate_tokens(text)
|
|
130
|
+
(text.to_s.bytesize / 4.0).round
|
|
131
|
+
end
|
|
132
|
+
end
|
|
@@ -8,10 +8,12 @@
|
|
|
8
8
|
# heartbeat. Never the raw ledger (296 D36). No environment reads; every
|
|
9
9
|
# path is injected.
|
|
10
10
|
|
|
11
|
+
require_relative "store_layout"
|
|
11
12
|
require "time"
|
|
12
13
|
require_relative "session_ledger"
|
|
13
14
|
require_relative "handoff"
|
|
14
15
|
require_relative "lock"
|
|
16
|
+
require_relative "active_delivery"
|
|
15
17
|
require_relative "report_screen"
|
|
16
18
|
|
|
17
19
|
module DaySummary
|
|
@@ -43,7 +45,7 @@ module DaySummary
|
|
|
43
45
|
open: open_items(store, day),
|
|
44
46
|
done: last_done(store, day),
|
|
45
47
|
live: live_intents(home, now: now),
|
|
46
|
-
others: active_sessions(store, session, now: now, ttl: heartbeat_ttl),
|
|
48
|
+
others: active_sessions(store, session, now: now, ttl: heartbeat_ttl, home: home),
|
|
47
49
|
}
|
|
48
50
|
hidden = Hash.new(0)
|
|
49
51
|
cap!(lists, hidden, :open, OPEN_CAP, keep: :newest)
|
|
@@ -81,10 +83,10 @@ module DaySummary
|
|
|
81
83
|
# The global store plus every projects/<slug>/store, each with its INDEX
|
|
82
84
|
# one level up, the way doctor enumerates them.
|
|
83
85
|
def stores(home)
|
|
84
|
-
list = [[File.join(home, "INDEX.md"),
|
|
85
|
-
projects_root =
|
|
86
|
+
list = [[File.join(Plastic::StoreLayout.global_root(home), "INDEX.md"), Plastic::StoreLayout.global_store(home)]]
|
|
87
|
+
projects_root = Plastic::StoreLayout.projects_root(home)
|
|
86
88
|
if File.directory?(projects_root)
|
|
87
|
-
|
|
89
|
+
Plastic::StoreLayout.project_slugs(home).each do |slug|
|
|
88
90
|
list << [File.join(projects_root, slug, "INDEX.md"), File.join(projects_root, slug, "store")]
|
|
89
91
|
end
|
|
90
92
|
end
|
|
@@ -142,7 +144,7 @@ module DaySummary
|
|
|
142
144
|
"(no savepoint yet)"
|
|
143
145
|
end
|
|
144
146
|
|
|
145
|
-
def active_sessions(store, session, now:, ttl:)
|
|
147
|
+
def active_sessions(store, session, now:, ttl:, home: nil)
|
|
146
148
|
tmp_root = SessionLedger.tmp_root(store)
|
|
147
149
|
return [] unless File.directory?(tmp_root)
|
|
148
150
|
|
|
@@ -155,7 +157,7 @@ module DaySummary
|
|
|
155
157
|
age = heartbeat_age(dir, now)
|
|
156
158
|
next if age.nil? || age > ttl
|
|
157
159
|
|
|
158
|
-
"- #{sid} (#{(age / 60).floor}m ago, on #{
|
|
160
|
+
"- #{sid} (#{(age / 60).floor}m ago, on #{delivering_label(store, home, sid, now)})"
|
|
159
161
|
end
|
|
160
162
|
rescue SystemCallError
|
|
161
163
|
[]
|
|
@@ -178,14 +180,21 @@ module DaySummary
|
|
|
178
180
|
nil
|
|
179
181
|
end
|
|
180
182
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
183
|
+
# The delivering intent's id, or "day ledger" (344 n2, D4-D7): the
|
|
184
|
+
# basename before "--" of ActiveDelivery.resolve_by_short_session's
|
|
185
|
+
# match, else the session records into today's day ledger.
|
|
186
|
+
def delivering_label(store, home, sid, now)
|
|
187
|
+
return "day ledger" if home.to_s.empty?
|
|
188
|
+
|
|
189
|
+
dir = ActiveDelivery.resolve_by_short_session(
|
|
190
|
+
global_store: store,
|
|
191
|
+
project_roots: ActiveDelivery.project_roots(home),
|
|
192
|
+
short_session: sid,
|
|
193
|
+
now: now
|
|
194
|
+
)
|
|
195
|
+
dir ? File.basename(dir).split("--", 2).first : "day ledger"
|
|
196
|
+
rescue StandardError
|
|
197
|
+
"day ledger"
|
|
189
198
|
end
|
|
190
199
|
|
|
191
200
|
# --- rendering, pure -------------------------------------------------------------
|
|
@@ -8,12 +8,13 @@
|
|
|
8
8
|
# the store, conventions, intent and CLI halves, so Doctor stays one class with
|
|
9
9
|
# one public surface and one definition of which checks are core (intent 36a).
|
|
10
10
|
|
|
11
|
+
require_relative "store_layout"
|
|
11
12
|
require "json"
|
|
12
13
|
require "yaml"
|
|
13
14
|
require "time"
|
|
14
15
|
require "digest"
|
|
15
|
-
require "rubygems"
|
|
16
16
|
|
|
17
|
+
require_relative "version_number"
|
|
17
18
|
require_relative "hook_registry"
|
|
18
19
|
require_relative "compact_instructions"
|
|
19
20
|
|
|
@@ -27,10 +28,16 @@ class Doctor
|
|
|
27
28
|
"hermes" => { name: "Hermes", dir: File.join(Dir.home, ".hermes") },
|
|
28
29
|
}.freeze
|
|
29
30
|
|
|
30
|
-
# The Claude events hooks_registered expects in settings.json: the
|
|
31
|
+
# The Claude events hooks_registered expects in settings.json: the eight-event map of
|
|
31
32
|
# cut-inventory 3b (intent 309 added SessionEnd, registered for close since intent 301;
|
|
32
|
-
# intent 316a added MessageDisplay, registered for message-display, Claude only
|
|
33
|
-
|
|
33
|
+
# intent 316a added MessageDisplay, registered for message-display, Claude only; intent
|
|
34
|
+
# 355 added PreToolUse, registered for call-budget, Claude only; intent 340b added Stop,
|
|
35
|
+
# registered for stop, Claude only). Stop's registration is static (D5/D7) even though
|
|
36
|
+
# its runtime arm defaults off (D9, runner.stop_hook), so an install missing it is
|
|
37
|
+
# exactly as broken as one missing PreCompact, and this list keeps hooks_registered and
|
|
38
|
+
# hooks_match_registry agreeing on that (row 4.32).
|
|
39
|
+
CLAUDE_HOOK_EVENTS = %w[SessionStart PreToolUse PreCompact PostToolUse UserPromptSubmit SessionEnd
|
|
40
|
+
MessageDisplay Stop].freeze
|
|
34
41
|
|
|
35
42
|
# Launchers the installer places in the agent's hooks dir that are NOT hooks
|
|
36
43
|
# (intent 204): plastic-statusline is the settings["statusLine"] command, wired
|
|
@@ -91,8 +98,19 @@ class Doctor
|
|
|
91
98
|
|
|
92
99
|
JSON.parse(File.read(path))
|
|
93
100
|
rescue JSON::ParserError
|
|
94
|
-
|
|
95
|
-
JSON
|
|
101
|
+
# The comment/trailing-comma-stripped retry below can itself raise
|
|
102
|
+
# JSON::ParserError on genuinely malformed content (a truncated file, or
|
|
103
|
+
# plain garbage). A nested begin/rescue is required here because a
|
|
104
|
+
# method-level `rescue` clause never catches an exception raised from
|
|
105
|
+
# INSIDE a sibling rescue clause's own body (only from the main body).
|
|
106
|
+
# Without this nesting a malformed settings.json crashes doctor instead
|
|
107
|
+
# of reporting a clean fail (intent 331e, F5).
|
|
108
|
+
begin
|
|
109
|
+
content = File.read(path).gsub(%r{//[^\n]*}, "").gsub(/,(\s*[}\]])/, '\1')
|
|
110
|
+
JSON.parse(content)
|
|
111
|
+
rescue
|
|
112
|
+
nil
|
|
113
|
+
end
|
|
96
114
|
rescue
|
|
97
115
|
nil
|
|
98
116
|
end
|
|
@@ -284,7 +302,7 @@ class Doctor
|
|
|
284
302
|
return checks
|
|
285
303
|
end
|
|
286
304
|
|
|
287
|
-
index_path = File.join(plastic_home, "INDEX.md")
|
|
305
|
+
index_path = File.join(Plastic::StoreLayout.global_root(plastic_home), "INDEX.md")
|
|
288
306
|
if File.exist?(index_path)
|
|
289
307
|
checks << check(
|
|
290
308
|
category: "global_store", name: "global_index_reachable", status: "pass",
|
|
@@ -395,7 +413,7 @@ class Doctor
|
|
|
395
413
|
fix_hint: "Re-run the Plastic installer: npx @zalom/plastic@latest --claude (prunes " \
|
|
396
414
|
"stale launchers). The plastic- prefix is reserved for Plastic's own hooks: " \
|
|
397
415
|
"if one of these is yours, rename it (for example to ~/.claude/hooks/" \
|
|
398
|
-
"writing
|
|
416
|
+
"plain-writing) and re-register it in settings.json before re-running."
|
|
399
417
|
)
|
|
400
418
|
end
|
|
401
419
|
|
|
@@ -500,9 +518,6 @@ class Doctor
|
|
|
500
518
|
checks << hooks_entries_owned_check(settings)
|
|
501
519
|
end
|
|
502
520
|
|
|
503
|
-
# skills_exist — flat, hyphen-namespaced personal skills (plastic-<name>/)
|
|
504
|
-
checks << flat_skills_check(agent_dir, "--claude")
|
|
505
|
-
|
|
506
521
|
# stray_skills — installed plastic-* skill dir with no manifest entry (a leftover,
|
|
507
522
|
# e.g. an old-name copy after a rename; intent 158a AC15)
|
|
508
523
|
checks << stray_skills_check(agent_dir, "--claude", File.join(agent_dir, "plastic", "manifest.json"))
|
|
@@ -613,24 +628,6 @@ class Doctor
|
|
|
613
628
|
)
|
|
614
629
|
end
|
|
615
630
|
|
|
616
|
-
def flat_skills_check(agent_dir, installer_flag)
|
|
617
|
-
skills_root = File.join(agent_dir, "skills")
|
|
618
|
-
found = Dir.glob(File.join(skills_root, "plastic-*", "SKILL.md"))
|
|
619
|
-
|
|
620
|
-
if !found.empty?
|
|
621
|
-
check(
|
|
622
|
-
category: "agent_registration", name: "skills_exist", status: "pass",
|
|
623
|
-
message: "#{found.size} plastic-* skill(s) installed in #{tilde(skills_root)}"
|
|
624
|
-
)
|
|
625
|
-
else
|
|
626
|
-
check(
|
|
627
|
-
category: "agent_registration", name: "skills_exist", status: "fail",
|
|
628
|
-
message: "No plastic-* skills found in #{tilde(skills_root)}",
|
|
629
|
-
fixable: true, fix_hint: "Re-run the Plastic installer: npx @zalom/plastic@latest #{installer_flag}"
|
|
630
|
-
)
|
|
631
|
-
end
|
|
632
|
-
end
|
|
633
|
-
|
|
634
631
|
# Manifest-diff stray-skill check (intent 158a), extended (intent 276) to
|
|
635
632
|
# never vanish on a missing manifest and to name the reserved-prefix rule.
|
|
636
633
|
def stray_skills_check(agent_dir, installer_flag, manifest_path)
|
|
@@ -690,9 +687,6 @@ class Doctor
|
|
|
690
687
|
def check_flat_skills_and_stray(agent_key, agent_dir)
|
|
691
688
|
checks = []
|
|
692
689
|
|
|
693
|
-
# For codex/hermes: just check skills exist (no settings.json hooks)
|
|
694
|
-
checks << flat_skills_check(agent_dir, "--#{agent_key}")
|
|
695
|
-
|
|
696
690
|
# stray_skills — installed plastic-* skill dir with no manifest entry (a leftover,
|
|
697
691
|
# e.g. an old-name copy after a rename; intent 158a AC15)
|
|
698
692
|
checks << stray_skills_check(agent_dir, "--#{agent_key}", File.join(agent_dir, "plastic", "manifest.json"))
|
|
@@ -1044,9 +1038,7 @@ class Doctor
|
|
|
1044
1038
|
end
|
|
1045
1039
|
|
|
1046
1040
|
def safe_version(str)
|
|
1047
|
-
|
|
1048
|
-
rescue ArgumentError
|
|
1049
|
-
nil
|
|
1041
|
+
VersionNumber.parse(str)
|
|
1050
1042
|
end
|
|
1051
1043
|
|
|
1052
1044
|
def check_core_files(agent_key, include_drift: true)
|
|
@@ -1150,7 +1142,7 @@ class Doctor
|
|
|
1150
1142
|
"#{tilde(agent_version_path)}: #{agent_version}",
|
|
1151
1143
|
],
|
|
1152
1144
|
fixable: true,
|
|
1153
|
-
fix_hint: "Re-sync the stale harness: npx @zalom/plastic@latest install --reinstall <flag>, or plastic
|
|
1145
|
+
fix_hint: "Re-sync the stale harness: npx @zalom/plastic@latest install --reinstall <flag>, or `plastic rollback` to a prior version"
|
|
1154
1146
|
)
|
|
1155
1147
|
end
|
|
1156
1148
|
else
|
|
@@ -1158,7 +1150,7 @@ class Doctor
|
|
|
1158
1150
|
category: "core_files", name: "version_match", status: "warn",
|
|
1159
1151
|
message: "Agent-side VERSION file not found at #{tilde(agent_version_path)}",
|
|
1160
1152
|
fixable: true,
|
|
1161
|
-
fix_hint: "Re-sync the stale harness: npx @zalom/plastic@latest install --reinstall <flag>, or plastic
|
|
1153
|
+
fix_hint: "Re-sync the stale harness: npx @zalom/plastic@latest install --reinstall <flag>, or `plastic rollback` to a prior version"
|
|
1162
1154
|
)
|
|
1163
1155
|
end
|
|
1164
1156
|
end
|
|
@@ -1240,15 +1232,99 @@ class Doctor
|
|
|
1240
1232
|
# agent_model_drift, no store-content scanning).
|
|
1241
1233
|
def run_core_checks(agent_key)
|
|
1242
1234
|
all_checks = []
|
|
1243
|
-
all_checks += check_agent_registration(agent_key)
|
|
1235
|
+
all_checks += check_agent_registration(agent_key).reject { |c| c[:name] == "codex_hooks_trust" }
|
|
1244
1236
|
all_checks += check_core_files(agent_key, include_drift: false)
|
|
1245
1237
|
all_checks += check_manifest_sync(agent_key)
|
|
1246
1238
|
all_checks += check_registered_project_paths
|
|
1247
1239
|
all_checks += check_global_store_available
|
|
1240
|
+
all_checks += check_display_registration(agent_key)
|
|
1248
1241
|
|
|
1249
1242
|
summarize(all_checks, agent_key, binary: true)
|
|
1250
1243
|
end
|
|
1251
1244
|
|
|
1245
|
+
# The single `hooks/<name>` launcher basename for the MessageDisplay event,
|
|
1246
|
+
# derived from HookRegistry rather than hand-kept (intent 331e), so a
|
|
1247
|
+
# future rename of the hook stays in one place. Shared by
|
|
1248
|
+
# check_display_registration (below, boot path) and scripts/doctor.rb's
|
|
1249
|
+
# check_display_paints (full run), which resolves the SAME name under the
|
|
1250
|
+
# agent_dir it was given.
|
|
1251
|
+
def display_hook_launcher_name
|
|
1252
|
+
group = HookRegistry.events["MessageDisplay"].first
|
|
1253
|
+
"plastic-#{group['hooks'].first['name']}"
|
|
1254
|
+
end
|
|
1255
|
+
|
|
1256
|
+
DISPLAY_HOOK_FIX_HINT = "Re-run the Plastic installer to repair the hook registration: " \
|
|
1257
|
+
"npx @zalom/plastic@<channel> install --reinstall --claude " \
|
|
1258
|
+
"(plastic install --reinstall)".freeze
|
|
1259
|
+
|
|
1260
|
+
# display_hook_registered (intent 331e, D1, category "display"): the Claude
|
|
1261
|
+
# settings carry the plastic-message-display command, on-disk, executable.
|
|
1262
|
+
# Boot-path safe: resolves everything from the injected `agents` hash and
|
|
1263
|
+
# `plastic_home`, never Dir.home or a real ~/.claude (E18). This is the same
|
|
1264
|
+
# discipline check_claude_registration already follows.
|
|
1265
|
+
#
|
|
1266
|
+
# D3: a harness Doctor knows carries no display hook (Codex, Hermes) is a
|
|
1267
|
+
# pass, not a fail, worded "plain by contract" like the paint check's own
|
|
1268
|
+
# skip (scripts/doctor.rb's check_display_paints).
|
|
1269
|
+
def check_display_registration(agent_key)
|
|
1270
|
+
config = agents[agent_key]
|
|
1271
|
+
unless agent_key == "claude"
|
|
1272
|
+
return [check(
|
|
1273
|
+
category: "display", name: "display_hook_registered", status: "pass",
|
|
1274
|
+
message: "#{config[:name]} is plain by contract; no MessageDisplay hook to register"
|
|
1275
|
+
)]
|
|
1276
|
+
end
|
|
1277
|
+
|
|
1278
|
+
agent_dir = config[:dir]
|
|
1279
|
+
settings_path = File.join(agent_dir, "settings.json")
|
|
1280
|
+
settings = read_json_safe(settings_path)
|
|
1281
|
+
|
|
1282
|
+
if settings.nil?
|
|
1283
|
+
return [check(
|
|
1284
|
+
category: "display", name: "display_hook_registered", status: "fail",
|
|
1285
|
+
message: "Cannot read #{tilde(settings_path)}: file missing or invalid",
|
|
1286
|
+
fixable: true, fix_hint: DISPLAY_HOOK_FIX_HINT
|
|
1287
|
+
)]
|
|
1288
|
+
end
|
|
1289
|
+
|
|
1290
|
+
hooks = settings["hooks"].is_a?(Hash) ? settings["hooks"] : {}
|
|
1291
|
+
commands = event_commands(hooks["MessageDisplay"])
|
|
1292
|
+
launcher_name = display_hook_launcher_name
|
|
1293
|
+
|
|
1294
|
+
registered = commands.any? { |cmd| HookRegistry.command_basenames(cmd).include?(launcher_name) }
|
|
1295
|
+
|
|
1296
|
+
unless registered
|
|
1297
|
+
return [check(
|
|
1298
|
+
category: "display", name: "display_hook_registered", status: "fail",
|
|
1299
|
+
message: "No MessageDisplay hook registered in #{tilde(settings_path)}",
|
|
1300
|
+
fixable: true, fix_hint: DISPLAY_HOOK_FIX_HINT
|
|
1301
|
+
)]
|
|
1302
|
+
end
|
|
1303
|
+
|
|
1304
|
+
launcher_path = File.join(agent_dir, "hooks", launcher_name)
|
|
1305
|
+
|
|
1306
|
+
unless File.exist?(launcher_path)
|
|
1307
|
+
return [check(
|
|
1308
|
+
category: "display", name: "display_hook_registered", status: "fail",
|
|
1309
|
+
message: "MessageDisplay is registered but #{tilde(launcher_path)} does not exist",
|
|
1310
|
+
fixable: true, fix_hint: DISPLAY_HOOK_FIX_HINT
|
|
1311
|
+
)]
|
|
1312
|
+
end
|
|
1313
|
+
|
|
1314
|
+
unless File.executable?(launcher_path)
|
|
1315
|
+
return [check(
|
|
1316
|
+
category: "display", name: "display_hook_registered", status: "fail",
|
|
1317
|
+
message: "#{tilde(launcher_path)} exists but is not executable",
|
|
1318
|
+
fixable: true, fix_hint: DISPLAY_HOOK_FIX_HINT
|
|
1319
|
+
)]
|
|
1320
|
+
end
|
|
1321
|
+
|
|
1322
|
+
[check(
|
|
1323
|
+
category: "display", name: "display_hook_registered", status: "pass",
|
|
1324
|
+
message: "MessageDisplay hook registered and #{tilde(launcher_path)} is executable"
|
|
1325
|
+
)]
|
|
1326
|
+
end
|
|
1327
|
+
|
|
1252
1328
|
def check_registered_project_paths
|
|
1253
1329
|
checks = []
|
|
1254
1330
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# encoding: UTF-8
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
+
require_relative "store_layout"
|
|
4
5
|
require "time"
|
|
5
6
|
require_relative "session_ledger"
|
|
6
7
|
|
|
@@ -15,21 +16,12 @@ require_relative "session_ledger"
|
|
|
15
16
|
# heartbeat is only refreshed on prompts and edits, so an old heartbeat is evidence, not
|
|
16
17
|
# proof, that the session is gone. Fail-open: an unreadable directory is skipped, never
|
|
17
18
|
# raised. Depends on the including class for `plastic_home` and `check`.
|
|
19
|
+
#
|
|
20
|
+
# Intent 344 (G11, D10): the third check this category used to carry,
|
|
21
|
+
# `no_pointer_session_tmp`, is retired along with the per-session state it read.
|
|
18
22
|
module DoctorSessionLedger
|
|
19
23
|
ORPHAN_TTL_SECONDS = 24 * 60 * 60
|
|
20
24
|
|
|
21
|
-
# Row H (spec D9): a `.tmp/<sid>/` directory with no `current` pointer is a
|
|
22
|
-
# DIFFERENT orphan class than ORPHAN_TTL_SECONDS's -- a session where
|
|
23
|
-
# session start never ran at all (a `-p` print session, a resumed
|
|
24
|
-
# background job, an unregistered SessionStart hook), not a session that
|
|
25
|
-
# ran and then never closed. No choice of session id ever creates a
|
|
26
|
-
# pointer for that class, so it would otherwise stay invisible for the
|
|
27
|
-
# full 24 hours (or forever, once hook-capture/hook-record stop creating
|
|
28
|
-
# it at all). Short relative to ORPHAN_TTL_SECONDS on purpose: a session
|
|
29
|
-
# between its own start and its first pointer write is normal and must not
|
|
30
|
-
# be flagged, but that window is seconds, not hours.
|
|
31
|
-
NO_POINTER_TTL_SECONDS = 5 * 60
|
|
32
|
-
|
|
33
25
|
# Seconds since the session's last heartbeat: the ISO-8601 content of `heartbeat`,
|
|
34
26
|
# else that file's mtime, else the directory's mtime.
|
|
35
27
|
def heartbeat_age(dir, now)
|
|
@@ -47,9 +39,8 @@ module DoctorSessionLedger
|
|
|
47
39
|
def check_session_ledger(scopes: nil, now: Time.now)
|
|
48
40
|
return [] unless scopes.nil? || scopes.include?("global")
|
|
49
41
|
|
|
50
|
-
store_dir =
|
|
42
|
+
store_dir = Plastic::StoreLayout.global_store(plastic_home)
|
|
51
43
|
orphans = orphaned_session_dirs(store_dir, now)
|
|
52
|
-
no_pointer = no_pointer_session_dirs(store_dir, now)
|
|
53
44
|
shape = day_ledger_shape_problems(store_dir)
|
|
54
45
|
|
|
55
46
|
checks = []
|
|
@@ -67,20 +58,6 @@ module DoctorSessionLedger
|
|
|
67
58
|
"session is gone: a live session rewrites its heartbeat on every " \
|
|
68
59
|
"prompt and edit, so only a listed directory may be removed, by hand.")
|
|
69
60
|
end
|
|
70
|
-
checks << if no_pointer.empty?
|
|
71
|
-
check(category: "session_ledger", name: "no_pointer_session_tmp", status: "pass",
|
|
72
|
-
message: "No .tmp/<session>/ directory has gone without a `current` pointer for " \
|
|
73
|
-
"longer than #{NO_POINTER_TTL_SECONDS} seconds")
|
|
74
|
-
else
|
|
75
|
-
check(category: "session_ledger", name: "no_pointer_session_tmp", status: "warn",
|
|
76
|
-
message: "#{no_pointer.size} .tmp/<session>/ director#{no_pointer.size == 1 ? "y" : "ies"} " \
|
|
77
|
-
"with no `current` pointer for longer than #{NO_POINTER_TTL_SECONDS} seconds " \
|
|
78
|
-
"(session start never ran for this session)",
|
|
79
|
-
details: no_pointer, fixable: true,
|
|
80
|
-
fix_hint: "Remove each listed .tmp/<session>/ directory after confirming that " \
|
|
81
|
-
"session never started: no `current` pointer means session start never " \
|
|
82
|
-
"ran for it, so this is not a live session missing a checklist entry.")
|
|
83
|
-
end
|
|
84
61
|
checks << if shape.empty?
|
|
85
62
|
check(category: "session_ledger", name: "day_ledger_shape", status: "pass",
|
|
86
63
|
message: "Every .sessions/ entry is a YYYYMMDD day directory with its <day>.md file")
|
|
@@ -113,31 +90,6 @@ module DoctorSessionLedger
|
|
|
113
90
|
[] # unreadable .tmp/: nothing to report, never a crash
|
|
114
91
|
end
|
|
115
92
|
|
|
116
|
-
# Row H (spec D9): `.tmp/<sid>/` directories with no `current` pointer, past
|
|
117
|
-
# NO_POINTER_TTL_SECONDS. A different signal than #orphaned_session_dirs:
|
|
118
|
-
# that one is age-only and blind to whether a pointer exists at all, so a
|
|
119
|
-
# young no-pointer dir (a session between its start and its first pointer
|
|
120
|
-
# write) must not appear here even though it may well appear there once it
|
|
121
|
-
# ages past ORPHAN_TTL_SECONDS -- the two checks answer different questions
|
|
122
|
-
# and a dir can legitimately show up in neither, either, or both.
|
|
123
|
-
def no_pointer_session_dirs(store_dir, now)
|
|
124
|
-
tmp_root = SessionLedger.tmp_root(store_dir)
|
|
125
|
-
return [] unless File.directory?(tmp_root)
|
|
126
|
-
|
|
127
|
-
Dir.children(tmp_root).sort.filter_map do |name|
|
|
128
|
-
dir = File.join(tmp_root, name)
|
|
129
|
-
next unless File.directory?(dir)
|
|
130
|
-
next if File.exist?(File.join(dir, "current"))
|
|
131
|
-
|
|
132
|
-
age = heartbeat_age(dir, now)
|
|
133
|
-
next if age <= NO_POINTER_TTL_SECONDS
|
|
134
|
-
|
|
135
|
-
"global: #{dir} (session #{name}, no `current` pointer, last heartbeat #{age.round}s ago)"
|
|
136
|
-
end
|
|
137
|
-
rescue SystemCallError
|
|
138
|
-
[] # unreadable .tmp/: nothing to report, never a crash
|
|
139
|
-
end
|
|
140
|
-
|
|
141
93
|
def day_ledger_shape_problems(store_dir)
|
|
142
94
|
root = SessionLedger.sessions_root(store_dir)
|
|
143
95
|
return [] unless File.directory?(root)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# EnginePermissions: the engine deny rule (intent 340b, G7c, n3). A permissions.deny
|
|
5
|
+
# block merged into settings.json at install so the engine directories are not
|
|
6
|
+
# editable by a dispatched agent (C25's self-preservation half).
|
|
7
|
+
#
|
|
8
|
+
# Claude Code accepts a Write(...) path rule and never consults it: Edit(...) is the
|
|
9
|
+
# rule that covers Write, MultiEdit and NotebookEdit. Every entry below names Edit,
|
|
10
|
+
# never Write, and every entry ends in /** because a bare directory path matches the
|
|
11
|
+
# directory itself, not the files under it.
|
|
12
|
+
#
|
|
13
|
+
# This is a second ownership mechanism, deliberately not a reuse of HookRegistry's
|
|
14
|
+
# merge, which knows its own entries by a plastic- launcher basename inside a command
|
|
15
|
+
# string. A deny entry is a bare string with no marker in it, so ownership here is
|
|
16
|
+
# exact-string membership in ENTRIES. The consequence is deliberate: an entry the
|
|
17
|
+
# owner has edited no longer matches the frozen string, so it is the owner's from
|
|
18
|
+
# that point on - the merge appends Plastic's own alongside it, and removal leaves
|
|
19
|
+
# the edited one in place untouched.
|
|
20
|
+
#
|
|
21
|
+
# Pure functions only: no file I/O here. InstallerCore#merge_engine_permissions and
|
|
22
|
+
# #remove_engine_permissions own the read-modify-write against settings.json,
|
|
23
|
+
# mirroring the split HookRegistry keeps from merge_claude_hooks.
|
|
24
|
+
module EnginePermissions
|
|
25
|
+
module_function
|
|
26
|
+
|
|
27
|
+
ENTRIES = [
|
|
28
|
+
"Edit(~/.plastic/scripts/**)",
|
|
29
|
+
"Edit(~/.plastic/skills/**)",
|
|
30
|
+
"Edit(~/.plastic/hooks/**)",
|
|
31
|
+
"Edit(~/.plastic/templates/**)",
|
|
32
|
+
].freeze
|
|
33
|
+
|
|
34
|
+
# Returns a new settings hash with ENTRIES merged into permissions.deny, appended
|
|
35
|
+
# (never duplicated) alongside whatever is already there. Survives a permissions
|
|
36
|
+
# or deny value that is not the shape expected (row 3.12): a non-Hash permissions
|
|
37
|
+
# value or a non-Array deny value is replaced rather than raised on. Never
|
|
38
|
+
# mutates the argument.
|
|
39
|
+
def merge_into(settings)
|
|
40
|
+
settings = settings.is_a?(Hash) ? settings.dup : {}
|
|
41
|
+
|
|
42
|
+
permissions = settings["permissions"]
|
|
43
|
+
permissions = permissions.is_a?(Hash) ? permissions.dup : {}
|
|
44
|
+
|
|
45
|
+
deny = permissions["deny"]
|
|
46
|
+
deny = deny.is_a?(Array) ? deny.dup : []
|
|
47
|
+
|
|
48
|
+
ENTRIES.each { |entry| deny << entry unless deny.include?(entry) }
|
|
49
|
+
|
|
50
|
+
permissions["deny"] = deny
|
|
51
|
+
settings["permissions"] = permissions
|
|
52
|
+
settings
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Returns a new settings hash with exactly the ENTRIES strings removed from
|
|
56
|
+
# permissions.deny, leaving every other entry (the owner's own deny rules, and
|
|
57
|
+
# any Plastic entry the owner has since edited) untouched. Prunes the deny array
|
|
58
|
+
# once it is empty, and the whole permissions block once nothing else remains
|
|
59
|
+
# under it (row 3.10). A settings/permissions/deny shape that is not what is
|
|
60
|
+
# expected is left alone rather than raised on (row 3.12's uninstall side).
|
|
61
|
+
def remove_from(settings)
|
|
62
|
+
return settings unless settings.is_a?(Hash)
|
|
63
|
+
|
|
64
|
+
permissions = settings["permissions"]
|
|
65
|
+
return settings unless permissions.is_a?(Hash)
|
|
66
|
+
|
|
67
|
+
deny = permissions["deny"]
|
|
68
|
+
return settings unless deny.is_a?(Array)
|
|
69
|
+
|
|
70
|
+
settings = settings.dup
|
|
71
|
+
permissions = permissions.dup
|
|
72
|
+
deny = deny.reject { |entry| ENTRIES.include?(entry) }
|
|
73
|
+
|
|
74
|
+
if deny.empty?
|
|
75
|
+
permissions.delete("deny")
|
|
76
|
+
else
|
|
77
|
+
permissions["deny"] = deny
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
if permissions.empty?
|
|
81
|
+
settings.delete("permissions")
|
|
82
|
+
else
|
|
83
|
+
settings["permissions"] = permissions
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
settings
|
|
87
|
+
end
|
|
88
|
+
end
|