@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,222 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# encoding: UTF-8
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
# Usage: hook-call-budget (reads the PreToolUse stdin JSON payload)
|
|
6
|
+
#
|
|
7
|
+
# Intent 355, n2 (review fix n8). A node gets a call budget by kind
|
|
8
|
+
# (RunnerPolicy.call_cap): a cap on tool calls per attempt. RunnerDispatch
|
|
9
|
+
# writes the cap onto the running line as `calls=` and states it as one
|
|
10
|
+
# sentence in the node's own input, so the executor learns the number before
|
|
11
|
+
# it ever gets denied.
|
|
12
|
+
#
|
|
13
|
+
# Inside a subagent the hook input's session_id and transcript_path are the
|
|
14
|
+
# MAIN session's, never the subagent's own (v1 finding). So this hook caps
|
|
15
|
+
# ONLY a subagent call: agent_id present in the payload. A call with no
|
|
16
|
+
# agent_id (the main thread, a plain conversation, a live --agent session's
|
|
17
|
+
# own turns) is never capped and the decision returns before any file is
|
|
18
|
+
# touched.
|
|
19
|
+
#
|
|
20
|
+
# Which node a subagent is working is read from the subagent's OWN
|
|
21
|
+
# transcript, at <dirname(transcript_path)>/<session_id>/subagents/
|
|
22
|
+
# agent-<agent_id>.jsonl (the main transcript_path plus the main session_id
|
|
23
|
+
# plus the subagent's own agent_id): its first user record carries the
|
|
24
|
+
# dispatch prompt, and the lead/spawn block always pastes the node's input
|
|
25
|
+
# path into that prompt (INPUT_PATH_RE). The node input path names the intent
|
|
26
|
+
# directory directly (its grandparent), so this hook never scans the store to
|
|
27
|
+
# find it. No node input path in that first prompt means allow.
|
|
28
|
+
#
|
|
29
|
+
# The cap applies only when that node's LATEST transition line in
|
|
30
|
+
# <intent>/savepoint.md is `running` and carries `calls=`; a later `done` or
|
|
31
|
+
# `failed_verification` line (or a sibling node's own running line) means no
|
|
32
|
+
# cap. Past the cap, a Bash call that runs only a safe git verb (add, commit,
|
|
33
|
+
# status, rev-parse, log) is still allowed, so the denial the hook itself
|
|
34
|
+
# orders (commit what is green) is never the thing the hook blocks.
|
|
35
|
+
#
|
|
36
|
+
# Pure and dependency-injected down to the paths it reads; the trailing block
|
|
37
|
+
# guarded by `$PROGRAM_NAME == __FILE__` is this file's only I/O (stdin,
|
|
38
|
+
# stdout), so a test can `load` this file and call HookCallBudget's module
|
|
39
|
+
# functions directly without spawning a process or touching stdin.
|
|
40
|
+
|
|
41
|
+
require "json"
|
|
42
|
+
require_relative "lib/node_ledger"
|
|
43
|
+
require_relative "lib/savepoint"
|
|
44
|
+
|
|
45
|
+
module HookCallBudget
|
|
46
|
+
module_function
|
|
47
|
+
|
|
48
|
+
RETURN_INSTRUCTION = "commit what is green and return failed_verification reason=call_budget"
|
|
49
|
+
|
|
50
|
+
# A node id, then "--a", then an attempt number, inside a /attempts/
|
|
51
|
+
# directory: the exact shape RunnerDispatch's spawn block pastes into a
|
|
52
|
+
# subagent's own first prompt. Captures [full_path, node].
|
|
53
|
+
INPUT_PATH_RE = %r{(\S+/attempts/([a-z]{1,2}\d+)--a\d+\.input)}.freeze
|
|
54
|
+
|
|
55
|
+
FIRST_RECORD_SCAN_LIMIT = 20
|
|
56
|
+
|
|
57
|
+
GIT_ALLOWED_VERBS = %w[add commit status rev-parse log].freeze
|
|
58
|
+
GIT_UNSAFE_RE = /[;&|\n]|\$\(|`/.freeze
|
|
59
|
+
|
|
60
|
+
# The one decision: nil (allow, no output) or a deny payload Hash. Never
|
|
61
|
+
# raises across its own boundary.
|
|
62
|
+
def decide(payload)
|
|
63
|
+
return nil unless payload.is_a?(Hash)
|
|
64
|
+
|
|
65
|
+
agent_id = payload["agent_id"]
|
|
66
|
+
return nil if agent_id.to_s.strip.empty?
|
|
67
|
+
|
|
68
|
+
session_id = payload["session_id"].to_s
|
|
69
|
+
transcript_path = payload["transcript_path"].to_s
|
|
70
|
+
return nil if session_id.empty? || transcript_path.empty?
|
|
71
|
+
|
|
72
|
+
sub_path = subagent_transcript_path(transcript_path, session_id, agent_id)
|
|
73
|
+
|
|
74
|
+
prompt = begin
|
|
75
|
+
first_user_prompt_text(sub_path)
|
|
76
|
+
rescue StandardError
|
|
77
|
+
nil
|
|
78
|
+
end
|
|
79
|
+
return nil unless prompt
|
|
80
|
+
|
|
81
|
+
match = INPUT_PATH_RE.match(prompt)
|
|
82
|
+
return nil unless match
|
|
83
|
+
|
|
84
|
+
node = match[2]
|
|
85
|
+
intent_dir = File.dirname(File.dirname(match[1]))
|
|
86
|
+
|
|
87
|
+
cap = node_cap(intent_dir, node)
|
|
88
|
+
return nil unless cap
|
|
89
|
+
|
|
90
|
+
count = begin
|
|
91
|
+
count_tool_use_blocks(sub_path)
|
|
92
|
+
rescue StandardError => e
|
|
93
|
+
note_unreadable(intent_dir, session_id, e.message)
|
|
94
|
+
return nil
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
return nil if count <= cap
|
|
98
|
+
return nil if git_only_bash_call?(payload)
|
|
99
|
+
|
|
100
|
+
deny_payload("#{node} is past its call budget (#{count}/#{cap} tool calls); #{RETURN_INSTRUCTION}")
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def subagent_transcript_path(main_transcript_path, session_id, agent_id)
|
|
104
|
+
File.join(File.dirname(main_transcript_path), session_id, "subagents", "agent-#{agent_id}.jsonl")
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# The first `"type":"user"` record in `path`'s own transcript, rendered to
|
|
108
|
+
# plain text (message.content as a string, or the joined text blocks of a
|
|
109
|
+
# content array). Scans at most FIRST_RECORD_SCAN_LIMIT lines: the dispatch
|
|
110
|
+
# prompt is always the transcript's opening record, so this never touches
|
|
111
|
+
# the bulk of a long-running subagent's history. Raises on a genuinely
|
|
112
|
+
# unreadable path (a directory, a permission error); the caller decides
|
|
113
|
+
# what that means.
|
|
114
|
+
def first_user_prompt_text(path)
|
|
115
|
+
File.open(path) do |f|
|
|
116
|
+
f.each_line.first(FIRST_RECORD_SCAN_LIMIT).each do |raw|
|
|
117
|
+
record = begin
|
|
118
|
+
JSON.parse(raw)
|
|
119
|
+
rescue JSON::ParserError
|
|
120
|
+
next
|
|
121
|
+
end
|
|
122
|
+
next unless record.is_a?(Hash) && record["type"] == "user"
|
|
123
|
+
|
|
124
|
+
return message_text(record)
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
nil
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def message_text(record)
|
|
131
|
+
content = record.dig("message", "content")
|
|
132
|
+
case content
|
|
133
|
+
when String
|
|
134
|
+
content
|
|
135
|
+
when Array
|
|
136
|
+
content.select { |b| b.is_a?(Hash) && b["type"] == "text" }.map { |b| b["text"].to_s }.join("\n")
|
|
137
|
+
else
|
|
138
|
+
""
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# The cap named on `node`'s LATEST (file-order) non-torn transition line in
|
|
143
|
+
# `intent_dir`/savepoint.md, or nil when that line is not `running`, carries
|
|
144
|
+
# no `calls=`, or does not exist at all (matrix 2.6/2.6b: a later done/
|
|
145
|
+
# failed_verification line, or a sibling node's own running line, never
|
|
146
|
+
# caps this node).
|
|
147
|
+
def node_cap(intent_dir, node)
|
|
148
|
+
path = File.join(intent_dir, "savepoint.md")
|
|
149
|
+
return nil unless File.exist?(path)
|
|
150
|
+
|
|
151
|
+
latest = NodeLedger.entries(path).select { |e| !e[:torn] && e[:subject] == node }.last
|
|
152
|
+
return nil unless latest && latest[:state] == "running"
|
|
153
|
+
|
|
154
|
+
cap = (latest[:fields] || {})["calls"]
|
|
155
|
+
cap ? cap.to_i : nil
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Streams `path` a line at a time, counting `"type":"tool_use"` content
|
|
159
|
+
# blocks with a plain regex rather than a full JSON parse per line (matrix
|
|
160
|
+
# 2.5/2.8): a 3 MB transcript counts in well under 50 ms this way.
|
|
161
|
+
def count_tool_use_blocks(path)
|
|
162
|
+
count = 0
|
|
163
|
+
File.foreach(path) { |ln| count += ln.scan(/"type"\s*:\s*"tool_use"/).length }
|
|
164
|
+
count
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Past the cap, a Bash call is still allowed when its command runs only one
|
|
168
|
+
# safe git verb (matrix 2.7/D3): the git add/commit the denial itself
|
|
169
|
+
# orders must never be the call the hook blocks. Any chaining token
|
|
170
|
+
# (`;`, `&`, `|`, a newline, a subshell or a backtick) refuses the whole
|
|
171
|
+
# command, so a compound command can never smuggle an unrelated call past
|
|
172
|
+
# the cap riding on a leading `git`.
|
|
173
|
+
def git_only_bash_call?(payload)
|
|
174
|
+
return false unless payload["tool_name"].to_s == "Bash"
|
|
175
|
+
|
|
176
|
+
command = payload.dig("tool_input", "command").to_s.strip
|
|
177
|
+
return false if command.empty? || GIT_UNSAFE_RE.match?(command)
|
|
178
|
+
|
|
179
|
+
tokens = command.split(/\s+/)
|
|
180
|
+
return false unless tokens.shift == "git"
|
|
181
|
+
|
|
182
|
+
tokens.shift(2) if tokens.first == "-C"
|
|
183
|
+
GIT_ALLOWED_VERBS.include?(tokens.first)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def deny_payload(reason)
|
|
187
|
+
{ "hookSpecificOutput" => { "hookEventName" => "PreToolUse", "permissionDecision" => "deny",
|
|
188
|
+
"permissionDecisionReason" => reason } }
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Best-effort note that the transcript could not be counted (matrix 2.9): a
|
|
192
|
+
# plain milestone line, the same shared primitive Commit/Review/Report
|
|
193
|
+
# lines already use, dedup'd on (stage, milestone) so a repeat failure in
|
|
194
|
+
# the same session with the same message is not written twice.
|
|
195
|
+
def note_unreadable(intent_dir, session_id, message, now: Time.now)
|
|
196
|
+
Savepoint.append_savepoint_line(intent_dir, "CallBudget", "transcript unreadable for #{session_id}: #{message}",
|
|
197
|
+
now)
|
|
198
|
+
rescue StandardError
|
|
199
|
+
nil
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
if $PROGRAM_NAME == __FILE__
|
|
204
|
+
raw = begin
|
|
205
|
+
$stdin.read
|
|
206
|
+
rescue StandardError
|
|
207
|
+
nil
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
if raw && !raw.strip.empty?
|
|
211
|
+
payload = begin
|
|
212
|
+
JSON.parse(raw)
|
|
213
|
+
rescue StandardError
|
|
214
|
+
nil
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
result = HookCallBudget.decide(payload)
|
|
218
|
+
puts JSON.generate(result) if result
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
exit 0
|
|
222
|
+
end
|
package/scripts/hook-capture
CHANGED
|
@@ -5,16 +5,18 @@
|
|
|
5
5
|
# Usage: hook-capture (reads the UserPromptSubmit stdin JSON payload)
|
|
6
6
|
# Intent 298. One UserPromptSubmit process replacing hook-continue,
|
|
7
7
|
# hook-future-intent-check, and hook-auto-arm: it appends a pending line to
|
|
8
|
-
# the session day ledger
|
|
9
|
-
#
|
|
10
|
-
#
|
|
8
|
+
# the session day ledger and detects "continue" and "auto" prompts. Every job
|
|
9
|
+
# runs in its own rescue and the hook always exits 0 (spec D2). Intent 345
|
|
10
|
+
# (D7, 323) removed the per-prompt Future-intent hint step entirely.
|
|
11
11
|
|
|
12
|
+
require_relative "lib/store_layout"
|
|
12
13
|
require "json"
|
|
13
|
-
require "yaml"
|
|
14
14
|
require "open3"
|
|
15
|
+
require "time"
|
|
15
16
|
require "fileutils"
|
|
16
17
|
require_relative "lib/session_ledger"
|
|
17
18
|
require_relative "lib/dashboard_banner"
|
|
19
|
+
require_relative "lib/active_delivery"
|
|
18
20
|
|
|
19
21
|
raw = begin
|
|
20
22
|
$stdin.read
|
|
@@ -37,124 +39,42 @@ cwd = payload["cwd"].to_s
|
|
|
37
39
|
cwd = Dir.pwd if cwd.empty?
|
|
38
40
|
|
|
39
41
|
plastic_home = File.expand_path(ENV["PLASTIC_HOME"] || "~/.plastic")
|
|
40
|
-
store =
|
|
42
|
+
store = Plastic::StoreLayout.global_store(plastic_home)
|
|
41
43
|
templates = File.expand_path("../templates", __dir__)
|
|
42
44
|
sid = SessionLedger.short_session_id(nil, session_id)
|
|
43
45
|
today = SessionLedger.day_id
|
|
44
46
|
|
|
45
|
-
# --- Extracted matching logic, shared by both the global and project store
|
|
46
|
-
# passes of job (f) below (mirrors hook-future-intent-check verbatim). ---
|
|
47
|
-
def future_intent_matches(store_root, message)
|
|
48
|
-
index_path = File.join(store_root, "INDEX.md")
|
|
49
|
-
return [] unless File.exist?(index_path)
|
|
50
|
-
|
|
51
|
-
lines = File.readlines(index_path)
|
|
52
|
-
future_dirs = []
|
|
53
|
-
section = nil
|
|
54
|
-
|
|
55
|
-
lines.each do |line|
|
|
56
|
-
section = :future if line.start_with?("## Future")
|
|
57
|
-
section = nil if line.start_with?("## ") && !line.start_with?("## Future")
|
|
58
|
-
next unless section == :future && line.strip.start_with?("- [")
|
|
59
|
-
|
|
60
|
-
future_dirs << $1 if line =~ /store\/([\w-]+)\//
|
|
61
|
-
end
|
|
62
|
-
return [] if future_dirs.empty?
|
|
63
|
-
|
|
64
|
-
matches = []
|
|
65
|
-
message_words = message.split(/\W+/).reject { |w| w.length < 4 }
|
|
66
|
-
|
|
67
|
-
future_dirs.each do |dir|
|
|
68
|
-
intent_path = File.join(store_root, "store", dir, "#{dir}.md")
|
|
69
|
-
next unless File.exist?(intent_path)
|
|
70
|
-
|
|
71
|
-
content = File.read(intent_path)
|
|
72
|
-
|
|
73
|
-
tags = []
|
|
74
|
-
tags = $1.split(",").map(&:strip).map(&:downcase) if content =~ /^tags:\s*\[([^\]]+)\]/
|
|
75
|
-
|
|
76
|
-
intent_name = ""
|
|
77
|
-
intent_name = $1.downcase if content =~ /^intent:\s*["']?(.+?)["']?\s*$/
|
|
78
|
-
|
|
79
|
-
keywords = (tags + intent_name.split(/\W+/).reject { |w| w.length < 4 }).map(&:downcase).uniq
|
|
80
|
-
|
|
81
|
-
matched_keywords = keywords.select { |kw| message.include?(kw) }
|
|
82
|
-
name_matches = message_words.select { |w| intent_name.include?(w) }
|
|
83
|
-
matched_keywords = (matched_keywords + name_matches).uniq
|
|
84
|
-
|
|
85
|
-
next if matched_keywords.empty?
|
|
86
|
-
|
|
87
|
-
index_line = lines.find { |l| l.include?(dir) }&.strip || "#{dir} - #{intent_name}"
|
|
88
|
-
matches << { "index_line" => index_line, "keywords" => matched_keywords }
|
|
89
|
-
end
|
|
90
|
-
matches
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
# The store_root that carries the current project's own store, resolved from
|
|
94
|
-
# cwd through projects.yml, mirroring SessionLedger.project_slug's matching
|
|
95
|
-
# but returning nil (not "global") when nothing matches, since there is then
|
|
96
|
-
# no distinct second store to hint against.
|
|
97
|
-
def resolve_project_store_root(cwd, plastic_home)
|
|
98
|
-
projects_path = File.join(plastic_home, "projects.yml")
|
|
99
|
-
return nil unless File.exist?(projects_path)
|
|
100
|
-
|
|
101
|
-
data = begin
|
|
102
|
-
YAML.safe_load(File.read(projects_path))
|
|
103
|
-
rescue StandardError
|
|
104
|
-
nil
|
|
105
|
-
end
|
|
106
|
-
data = {} unless data.is_a?(Hash)
|
|
107
|
-
projects = data["projects"].is_a?(Hash) ? data["projects"] : {}
|
|
108
|
-
expanded_cwd = File.expand_path(cwd)
|
|
109
|
-
|
|
110
|
-
matches = projects.filter_map do |slug, info|
|
|
111
|
-
next unless info.is_a?(Hash) && slug.is_a?(String)
|
|
112
|
-
|
|
113
|
-
path = info["path"]
|
|
114
|
-
next unless path
|
|
115
|
-
|
|
116
|
-
root = File.expand_path(path)
|
|
117
|
-
next unless expanded_cwd == root || expanded_cwd.start_with?("#{root}#{File::SEPARATOR}")
|
|
118
|
-
|
|
119
|
-
[root.length, slug]
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
best = matches.max_by { |(length, _slug)| length }
|
|
123
|
-
best ? File.join(plastic_home, "projects", best[1]) : nil
|
|
124
|
-
end
|
|
125
|
-
|
|
126
47
|
def truncate(text, max)
|
|
127
48
|
return text if text.length <= max
|
|
128
49
|
|
|
129
50
|
"#{text[0, max - 3]}..."
|
|
130
51
|
end
|
|
131
52
|
|
|
132
|
-
# --- (a)
|
|
133
|
-
# Session start
|
|
53
|
+
# --- (a) heartbeat, guarded on the session tmp dir already existing --------
|
|
54
|
+
# Session start creates `.tmp/<sid>/`; a capture with no such directory yet
|
|
134
55
|
# means session start never ran for this session at all (a `-p` print
|
|
135
56
|
# session, a resumed background job, an unregistered SessionStart hook), so
|
|
136
|
-
# neither the
|
|
137
|
-
# fires before PostToolUse, so capture is the FIRST
|
|
138
|
-
#
|
|
139
|
-
#
|
|
57
|
+
# neither the directory nor its heartbeat is created HERE either.
|
|
58
|
+
# UserPromptSubmit fires before PostToolUse, so capture is the FIRST place
|
|
59
|
+
# this guard matters; hook-record carries the same guard for its own
|
|
60
|
+
# (later) heartbeat write. (344 n2, D4-D5)
|
|
140
61
|
begin
|
|
141
|
-
if
|
|
142
|
-
SessionLedger.ensure_tmp_root(store)
|
|
143
|
-
FileUtils.mkdir_p(SessionLedger.session_tmp_dir(store, sid))
|
|
62
|
+
if Dir.exist?(SessionLedger.session_tmp_dir(store, sid))
|
|
144
63
|
File.write(SessionLedger.heartbeat_path(store, sid), "#{Time.now.utc.iso8601}\n")
|
|
145
64
|
end
|
|
146
65
|
rescue StandardError
|
|
147
66
|
nil
|
|
148
67
|
end
|
|
149
68
|
|
|
150
|
-
# --- (b)
|
|
69
|
+
# --- (b) a live delivery lock names this session (344 n2, D5, D12) ----------
|
|
151
70
|
skip_day_ledger = false
|
|
152
71
|
begin
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
72
|
+
full_session = session_id.empty? ? ENV["CLAUDE_CODE_SESSION_ID"].to_s : session_id
|
|
73
|
+
skip_day_ledger = !ActiveDelivery.resolve(
|
|
74
|
+
global_store: store,
|
|
75
|
+
project_roots: ActiveDelivery.project_roots(plastic_home),
|
|
76
|
+
session: full_session
|
|
77
|
+
).nil?
|
|
158
78
|
rescue StandardError
|
|
159
79
|
nil
|
|
160
80
|
end
|
|
@@ -183,13 +103,13 @@ system_message = nil
|
|
|
183
103
|
|
|
184
104
|
# --- (d) "continue" cockpit -------------------------------------------------
|
|
185
105
|
begin
|
|
186
|
-
if prompt.
|
|
106
|
+
if prompt.to_s.strip.downcase == "continue"
|
|
187
107
|
dashboard = File.expand_path("dashboard.rb", __dir__)
|
|
188
108
|
if File.exist?(dashboard)
|
|
189
109
|
cockpit, _err, status = Open3.capture3({ "RUBYOPT" => nil }, "ruby", dashboard, "continue")
|
|
190
110
|
if status.success? && !cockpit.strip.empty?
|
|
191
111
|
context_parts << (cockpit.rstrip +
|
|
192
|
-
"\n\
|
|
112
|
+
"\n\nRun `plastic continue` to resume: " \
|
|
193
113
|
"if active intents exist, read their state and resume; otherwise surface the " \
|
|
194
114
|
"most-valuable next work and any stale or triage intents from the board above.")
|
|
195
115
|
|
|
@@ -216,7 +136,7 @@ begin
|
|
|
216
136
|
auto_triggers = ["take it from here", "deliver this", "/plastic-auto", "auto mode"].freeze
|
|
217
137
|
triggered = auto_triggers.any? { |p| message_lower.include?(p) } || message_lower.match?(/\bauto\b/)
|
|
218
138
|
if triggered
|
|
219
|
-
context_parts << "Auto-mode request detected.
|
|
139
|
+
context_parts << "Auto-mode request detected. Run `plastic auto take ID` to deliver the " \
|
|
220
140
|
"active intent autonomously - it takes the intent under a lock and a worktree " \
|
|
221
141
|
"and points this session at it."
|
|
222
142
|
end
|
|
@@ -224,25 +144,6 @@ rescue StandardError
|
|
|
224
144
|
nil
|
|
225
145
|
end
|
|
226
146
|
|
|
227
|
-
# --- (f) Future-intent hint, global store then the project's own store -----
|
|
228
|
-
begin
|
|
229
|
-
message = prompt.to_s.downcase
|
|
230
|
-
if message.strip.length >= 10 && message.strip != "continue"
|
|
231
|
-
matches = future_intent_matches(plastic_home, message)
|
|
232
|
-
project_root = resolve_project_store_root(cwd, plastic_home)
|
|
233
|
-
matches += future_intent_matches(project_root, message) if project_root
|
|
234
|
-
|
|
235
|
-
if matches.any?
|
|
236
|
-
parts = ["PLASTIC - Future intents related to this message:\n"]
|
|
237
|
-
matches.each { |m| parts << "#{m["index_line"]} (matched: #{m["keywords"].join(", ")})" }
|
|
238
|
-
parts << "\nConsider asking the user if they want to activate any of these, or note the connection."
|
|
239
|
-
context_parts << parts.join("\n")
|
|
240
|
-
end
|
|
241
|
-
end
|
|
242
|
-
rescue StandardError
|
|
243
|
-
nil
|
|
244
|
-
end
|
|
245
|
-
|
|
246
147
|
exit 0 if context_parts.empty?
|
|
247
148
|
|
|
248
149
|
payload_out = {
|
package/scripts/hook-close
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
#
|
|
9
9
|
# Usage: hook-close <plastic_home> (stdin: the hook JSON)
|
|
10
10
|
|
|
11
|
+
require_relative "lib/store_layout"
|
|
11
12
|
require "json"
|
|
12
13
|
require_relative "lib/session_ledger"
|
|
13
14
|
require_relative "lib/session_close"
|
|
@@ -23,7 +24,7 @@ payload =
|
|
|
23
24
|
{}
|
|
24
25
|
end
|
|
25
26
|
|
|
26
|
-
store =
|
|
27
|
+
store = Plastic::StoreLayout.global_store(File.expand_path(plastic_home))
|
|
27
28
|
filer = File.expand_path("file-session-intent", __dir__)
|
|
28
29
|
templates = File.expand_path("../templates", __dir__)
|
|
29
30
|
begin
|
|
@@ -51,11 +51,18 @@ begin
|
|
|
51
51
|
tmp_root = ENV["PLASTIC_TMP"].to_s.empty? ? Dir.tmpdir : ENV["PLASTIC_TMP"]
|
|
52
52
|
plastic_home = File.expand_path(ENV["PLASTIC_HOME"] || "~/.plastic")
|
|
53
53
|
|
|
54
|
+
# PLASTIC_HOOK_TRACE=<file> (331a1): opt-in, off by default, one JSON
|
|
55
|
+
# object appended per chunk. This script is the only place allowed to
|
|
56
|
+
# read the environment; MessageDisplay takes the sink as an argument.
|
|
57
|
+
trace_path = ENV["PLASTIC_HOOK_TRACE"].to_s
|
|
58
|
+
trace = trace_path.empty? ? nil : MessageDisplay.file_trace(trace_path)
|
|
59
|
+
|
|
54
60
|
handler = MessageDisplay.new(
|
|
55
61
|
tmp_root: tmp_root,
|
|
56
62
|
plastic_home: plastic_home,
|
|
57
63
|
color: color_enabled?(plastic_home),
|
|
58
64
|
now: Time.now,
|
|
65
|
+
trace: trace,
|
|
59
66
|
)
|
|
60
67
|
result = handler.handle(payload)
|
|
61
68
|
|
package/scripts/hook-record
CHANGED
|
@@ -10,12 +10,14 @@
|
|
|
10
10
|
# lands, and calls the session-commit seam when that script exists. Always
|
|
11
11
|
# exits 0 (spec D3).
|
|
12
12
|
|
|
13
|
+
require_relative "lib/store_layout"
|
|
13
14
|
require "json"
|
|
14
15
|
require "timeout"
|
|
15
16
|
require "fileutils"
|
|
16
17
|
require_relative "lib/savepoint"
|
|
17
18
|
require_relative "lib/lock"
|
|
18
19
|
require_relative "lib/session_ledger"
|
|
20
|
+
require_relative "lib/active_delivery"
|
|
19
21
|
|
|
20
22
|
raw = begin
|
|
21
23
|
$stdin.read
|
|
@@ -76,15 +78,24 @@ skip_day_ledger = inside_plastic || !intent_dir_abs.nil?
|
|
|
76
78
|
# --- (c) promote the newest pending line, Item savepoint, seam -------------
|
|
77
79
|
unless skip_day_ledger
|
|
78
80
|
begin
|
|
79
|
-
store =
|
|
81
|
+
store = Plastic::StoreLayout.global_store(plastic_home)
|
|
80
82
|
sid = SessionLedger.short_session_id(nil, session_id)
|
|
81
83
|
today = SessionLedger.day_id
|
|
82
84
|
|
|
85
|
+
# A live delivery lock naming this session's full id skips promotion
|
|
86
|
+
# (344 n2, D5, D12): the lock, not the retired pointer, decides.
|
|
83
87
|
proceed = true
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
+
unless session_id.empty?
|
|
89
|
+
begin
|
|
90
|
+
held = ActiveDelivery.resolve(
|
|
91
|
+
global_store: store,
|
|
92
|
+
project_roots: ActiveDelivery.project_roots(plastic_home),
|
|
93
|
+
session: session_id
|
|
94
|
+
)
|
|
95
|
+
proceed = held.nil?
|
|
96
|
+
rescue StandardError
|
|
97
|
+
proceed = true
|
|
98
|
+
end
|
|
88
99
|
end
|
|
89
100
|
|
|
90
101
|
if proceed
|
|
@@ -135,19 +146,16 @@ unless skip_day_ledger
|
|
|
135
146
|
end
|
|
136
147
|
end
|
|
137
148
|
|
|
138
|
-
# --- (d) heartbeat, guarded on the
|
|
139
|
-
# Session start
|
|
140
|
-
# start never ran for this session (a `-p` print
|
|
141
|
-
# background job, an unregistered SessionStart hook), so
|
|
142
|
-
#
|
|
143
|
-
#
|
|
144
|
-
# fallback reports as an orphan just the same.
|
|
149
|
+
# --- (d) heartbeat, guarded on the session tmp dir (344 n2, D4) ------------
|
|
150
|
+
# Session start creates the per-session tmp directory; with no such
|
|
151
|
+
# directory at all, session start never ran for this session (a `-p` print
|
|
152
|
+
# session, a resumed background job, an unregistered SessionStart hook), so
|
|
153
|
+
# the heartbeat is not written and no directory is created here either --
|
|
154
|
+
# the session tmp directory, created by session start, is the guard.
|
|
145
155
|
begin
|
|
146
|
-
store ||=
|
|
156
|
+
store ||= Plastic::StoreLayout.global_store(plastic_home)
|
|
147
157
|
sid ||= SessionLedger.short_session_id(nil, session_id)
|
|
148
|
-
if
|
|
149
|
-
SessionLedger.ensure_tmp_root(store)
|
|
150
|
-
FileUtils.mkdir_p(SessionLedger.session_tmp_dir(store, sid))
|
|
158
|
+
if Dir.exist?(SessionLedger.session_tmp_dir(store, sid))
|
|
151
159
|
File.write(SessionLedger.heartbeat_path(store, sid), "#{Time.now.utc.iso8601}\n")
|
|
152
160
|
end
|
|
153
161
|
rescue StandardError
|
package/scripts/hook-savepoint
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
# hook-savepoint (intent 311): the PreCompact hook body. Reads the harness
|
|
6
6
|
# payload from stdin (session_id, cwd), takes the Plastic home from argv,
|
|
7
|
-
# writes this session's hand-off for the
|
|
7
|
+
# writes this session's hand-off for the session's day from the day ledger,
|
|
8
|
+
# and prints one
|
|
8
9
|
# message: the fixed text, plus the written file's path when a session
|
|
9
10
|
# resolved (both harnesses relay the same launcher and the Codex pin feeds
|
|
10
11
|
# both sides the same payload, so the two stay equal). Always exits 0,
|
|
@@ -12,15 +13,35 @@
|
|
|
12
13
|
#
|
|
13
14
|
# Usage: hook-savepoint <plastic_home> (stdin: the hook JSON)
|
|
14
15
|
|
|
16
|
+
require_relative "lib/store_layout"
|
|
15
17
|
require "json"
|
|
16
18
|
require_relative "lib/session_ledger"
|
|
17
19
|
require_relative "lib/handoff"
|
|
20
|
+
require_relative "lib/active_delivery"
|
|
18
21
|
|
|
19
22
|
MESSAGE = "PLASTIC SAVEPOINT - context is being compacted. The hand-off for this " \
|
|
20
23
|
"session is written in today's day ledger " \
|
|
21
24
|
"(store/.sessions/<day>/handoff--<session>.md). " \
|
|
22
25
|
"After compaction say continue to resume from it."
|
|
23
26
|
|
|
27
|
+
# The intent this session is delivering, resolved through the same walk the
|
|
28
|
+
# Stop gate uses (row 4.38): read-config's project_roots, plus the global
|
|
29
|
+
# store, glob for a live delivery.lock this session holds. Never raises
|
|
30
|
+
# (ActiveDelivery.resolve rescues its own reads); a lookup failure just
|
|
31
|
+
# means the Runner section is omitted, never that the hand-off is lost.
|
|
32
|
+
def resolve_intent(plastic_home, session_id, read_config_path)
|
|
33
|
+
return nil if session_id.strip.empty?
|
|
34
|
+
|
|
35
|
+
global_store = Plastic::StoreLayout.global_store(plastic_home)
|
|
36
|
+
roots_raw = IO.popen({ "PLASTIC_HOME" => plastic_home }, [read_config_path, "project_roots"],
|
|
37
|
+
err: File::NULL, &:read).to_s.strip
|
|
38
|
+
project_roots = roots_raw.empty? ? [] : Array(JSON.parse(roots_raw))
|
|
39
|
+
project_roots = project_roots.map { |root| File.expand_path(root.to_s) }
|
|
40
|
+
ActiveDelivery.resolve(global_store: global_store, project_roots: project_roots, session: session_id)
|
|
41
|
+
rescue StandardError
|
|
42
|
+
nil
|
|
43
|
+
end
|
|
44
|
+
|
|
24
45
|
written = nil
|
|
25
46
|
begin
|
|
26
47
|
plastic_home = ARGV[0].to_s
|
|
@@ -30,11 +51,14 @@ begin
|
|
|
30
51
|
session_id = payload["session_id"].to_s
|
|
31
52
|
|
|
32
53
|
unless plastic_home.empty? || session_id.strip.empty?
|
|
33
|
-
|
|
54
|
+
home = File.expand_path(plastic_home)
|
|
55
|
+
store = Plastic::StoreLayout.global_store(home)
|
|
34
56
|
session = SessionLedger.short_session_id(session_id, nil)
|
|
35
57
|
day = Handoff.day_for(store, session, today: SessionLedger.day_id)
|
|
58
|
+
read_config_path = File.expand_path("read-config", __dir__)
|
|
59
|
+
intent = resolve_intent(home, session_id, read_config_path)
|
|
36
60
|
written = Handoff.write(store: store, day: day, session: session, trigger: "precompact",
|
|
37
|
-
templates: File.expand_path("../templates", __dir__))
|
|
61
|
+
templates: File.expand_path("../templates", __dir__), intent: intent)
|
|
38
62
|
end
|
|
39
63
|
rescue StandardError, JSON::ParserError
|
|
40
64
|
nil
|