@zalom/plastic 2.0.0-alpha.9 → 2.0.2
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 +21 -16
- 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 +8 -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/docs/help/completion-and-done.md +53 -0
- 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 +5 -4
- 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 +378 -56
- 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 +74 -91
- 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 +111 -0
- package/scripts/lib/cli/commands/auto_report.rb +50 -0
- package/scripts/lib/cli/commands/auto_take.rb +68 -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 +58 -0
- package/scripts/lib/cli/commands/intent_end.rb +75 -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 +79 -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 +46 -0
- package/scripts/lib/cli/commands/project_list.rb +20 -0
- package/scripts/lib/cli/commands/project_new.rb +95 -0
- package/scripts/lib/cli/commands/query.rb +31 -0
- package/scripts/lib/cli/commands/render.rb +35 -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 +77 -0
- package/scripts/lib/cli/commands/roadmap_show.rb +65 -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 +55 -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 +235 -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 +543 -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 +921 -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 +170 -12
- 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 +526 -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 +67 -19
- 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/untouched_scaffold.rb +51 -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 +65 -60
- package/scripts/project-links +6 -21
- 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 +125 -0
- package/scripts/roadmap-savepoint +7 -0
- package/scripts/rollback.rb +5 -1
- 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 +31 -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/conventions/references/completion-and-done.md +0 -43
- 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,249 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# encoding: UTF-8
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
require "json"
|
|
6
|
+
require "yaml"
|
|
7
|
+
require "date"
|
|
8
|
+
require_relative "lib/savepoint"
|
|
9
|
+
|
|
10
|
+
# graph-measure - the one read-only executable over the node ledger (intent
|
|
11
|
+
# 343, G10, n2). A subcommand table over one library, following
|
|
12
|
+
# scripts/runner's shape (327 spec): the public verbs are named up front even
|
|
13
|
+
# before every module lands, a flag outside a verb's own vocabulary is
|
|
14
|
+
# refused by name, and each verb's module is required LAZILY inside its own
|
|
15
|
+
# branch so a verb whose module has not landed yet reports plainly instead of
|
|
16
|
+
# taking `intent`, the verb an operator runs constantly, down with it.
|
|
17
|
+
#
|
|
18
|
+
# Usage:
|
|
19
|
+
# graph-measure <intent|budget|cohorts> <dir> [--format text|json]
|
|
20
|
+
#
|
|
21
|
+
# `intent` (n2) and `budget` (n4) take an INTENT directory. `cohorts` (n5,
|
|
22
|
+
# its model section; n6 adds the rates, the latency and the bar to the same
|
|
23
|
+
# verb) takes a STORE directory instead - the directory holding `id--slug/`
|
|
24
|
+
# intent folders for one project - and is validated against that shape, not
|
|
25
|
+
# `intent_directory?`.
|
|
26
|
+
#
|
|
27
|
+
# Exit codes:
|
|
28
|
+
# 0 - a report rendered (anomalies never change this: a ledger with torn
|
|
29
|
+
# lines still renders, lists them, and exits 0)
|
|
30
|
+
# 1 - the directory has no savepoint.md to measure at all, named
|
|
31
|
+
# 2 - usage: no args, an unknown subcommand, an unknown flag, or the path
|
|
32
|
+
# is not the right shape for its verb (an intent directory for
|
|
33
|
+
# `intent`/`budget`, a store directory for `cohorts`)
|
|
34
|
+
# 3 - the verb's module has not been delivered yet
|
|
35
|
+
module GraphMeasureCLI
|
|
36
|
+
module_function
|
|
37
|
+
|
|
38
|
+
VERBS = %w[intent budget cohorts].freeze
|
|
39
|
+
SAVEPOINT_FILE = "savepoint.md"
|
|
40
|
+
|
|
41
|
+
# verb -> [[ModuleName, lib_file], ...], required lazily inside the verb's
|
|
42
|
+
# own branch (row 2.6). "budget" and "cohorts" name libraries n4 and n5
|
|
43
|
+
# ship; requiring them today raises LoadError, caught below.
|
|
44
|
+
LAZY_MODULES = {
|
|
45
|
+
"intent" => [["GraphMeasure", "graph_measure"], ["GraphMeasureReport", "graph_measure_report"]],
|
|
46
|
+
"budget" => [["GraphMeasureBudget", "graph_measure_budget"]],
|
|
47
|
+
"cohorts" => [["GraphMeasureModels", "graph_measure_models"], ["GraphMeasureCohorts", "graph_measure_cohorts"]],
|
|
48
|
+
}.freeze
|
|
49
|
+
|
|
50
|
+
KNOWN_FLAGS = {
|
|
51
|
+
"intent" => %w[--format],
|
|
52
|
+
"budget" => %w[--format],
|
|
53
|
+
"cohorts" => %w[--format],
|
|
54
|
+
}.freeze
|
|
55
|
+
|
|
56
|
+
# "intent" and "budget" take one intent directory; "cohorts" takes the
|
|
57
|
+
# store directory that holds many of them (row 5.15).
|
|
58
|
+
DIR_KIND = { "intent" => :intent, "budget" => :intent, "cohorts" => :store }.freeze
|
|
59
|
+
|
|
60
|
+
def usage
|
|
61
|
+
warn "Usage: graph-measure <#{VERBS.join('|')}> <dir> [--format text|json]"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def intent_directory?(dir)
|
|
65
|
+
dir && File.directory?(dir) && File.exist?(Savepoint.intent_file(dir))
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def store_directory?(dir)
|
|
69
|
+
dir && File.directory?(dir)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def valid_dir?(verb, dir)
|
|
73
|
+
DIR_KIND.fetch(verb, :intent) == :store ? store_directory?(dir) : intent_directory?(dir)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def opt(args, name)
|
|
77
|
+
(i = args.index(name)) && args[i + 1]
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def unrecognized_flag(verb, args)
|
|
81
|
+
allowed = KNOWN_FLAGS.fetch(verb, [])
|
|
82
|
+
args.find { |a| a.is_a?(String) && a.start_with?("--") && !allowed.include?(a) }
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def main(argv)
|
|
86
|
+
args = argv.dup
|
|
87
|
+
verb = args.shift
|
|
88
|
+
|
|
89
|
+
unless verb
|
|
90
|
+
usage
|
|
91
|
+
return 2
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
unless VERBS.include?(verb)
|
|
95
|
+
warn "graph-measure: unknown subcommand #{verb.inspect}"
|
|
96
|
+
usage
|
|
97
|
+
return 2
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
dir_arg = args.shift
|
|
101
|
+
unless valid_dir?(verb, dir_arg && File.expand_path(dir_arg))
|
|
102
|
+
label = DIR_KIND.fetch(verb, :intent) == :store ? "not a store directory" : "not an intent directory"
|
|
103
|
+
warn "graph-measure: #{dir_arg.inspect} is #{label}"
|
|
104
|
+
usage
|
|
105
|
+
return 2
|
|
106
|
+
end
|
|
107
|
+
dir = File.expand_path(dir_arg)
|
|
108
|
+
|
|
109
|
+
bad_flag = unrecognized_flag(verb, args)
|
|
110
|
+
if bad_flag
|
|
111
|
+
warn "graph-measure: unknown flag #{bad_flag.inspect} for #{verb}"
|
|
112
|
+
usage
|
|
113
|
+
return 2
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
format = opt(args, "--format") || "text"
|
|
117
|
+
unless %w[text json].include?(format)
|
|
118
|
+
warn "graph-measure: unknown --format #{format.inspect} (use text or json)"
|
|
119
|
+
usage
|
|
120
|
+
return 2
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
run_lazy(verb, dir, format)
|
|
124
|
+
rescue StandardError => e
|
|
125
|
+
# Row 2.22: a malformed graph.md, a torn ledger, or invalid UTF-8 never
|
|
126
|
+
# raises out of GraphMeasure or GraphMeasureReport (both are built to
|
|
127
|
+
# resolve broken data to an anomaly, never an exception) - this is
|
|
128
|
+
# defence in depth so a defect in either never reaches the owner as a
|
|
129
|
+
# bare Ruby backtrace.
|
|
130
|
+
warn "graph-measure: internal error: #{e.message}"
|
|
131
|
+
1
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def run_lazy(verb, dir, format)
|
|
135
|
+
LAZY_MODULES.fetch(verb, []).each { |(_name, file)| require_relative(File.join("lib", file)) }
|
|
136
|
+
rescue LoadError
|
|
137
|
+
warn "graph-measure: #{verb} is not yet delivered (its module has not landed)"
|
|
138
|
+
3
|
|
139
|
+
else
|
|
140
|
+
dispatch_lazy(verb, dir, format)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def dispatch_lazy(verb, dir, format)
|
|
144
|
+
case verb
|
|
145
|
+
when "intent"
|
|
146
|
+
run_intent(dir, format)
|
|
147
|
+
when "budget"
|
|
148
|
+
run_budget(dir, format)
|
|
149
|
+
when "cohorts"
|
|
150
|
+
run_cohorts(dir, format)
|
|
151
|
+
else
|
|
152
|
+
warn "graph-measure: #{verb}'s module loaded but no dispatcher is wired up yet"
|
|
153
|
+
3
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# M1: the boundary's own config resolution, never GraphMeasureModels' or
|
|
158
|
+
# GraphMeasureCohorts' (spec D2, both modules' own DI convention). Mirrors
|
|
159
|
+
# scripts/doctor.rb's own `PLASTIC_HOME` fallback and `load_yaml_safe`
|
|
160
|
+
# shape exactly, so a missing or malformed config.yml/project.yml resolves
|
|
161
|
+
# to `{}` (the shipped-default chain), never an exception.
|
|
162
|
+
def plastic_home
|
|
163
|
+
ENV.fetch("PLASTIC_HOME") { File.join(Dir.home, ".plastic") }
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def load_yaml_safe(path)
|
|
167
|
+
return {} unless path && File.exist?(path)
|
|
168
|
+
|
|
169
|
+
YAML.safe_load(File.read(path), permitted_classes: [Date, Time]) || {}
|
|
170
|
+
rescue StandardError
|
|
171
|
+
{}
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def missing_savepoint(dir)
|
|
175
|
+
savepoint_path = File.join(dir, SAVEPOINT_FILE)
|
|
176
|
+
return nil if File.exist?(savepoint_path)
|
|
177
|
+
|
|
178
|
+
warn "graph-measure: #{savepoint_path} does not exist, nothing to measure"
|
|
179
|
+
savepoint_path
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def run_intent(dir, format)
|
|
183
|
+
# Row 2.5: checked directly, ahead of GraphMeasure.read, which resolves a
|
|
184
|
+
# missing file to an empty (not absent) record by design (D3) - the CLI
|
|
185
|
+
# boundary is the only place that can still tell "nothing to measure"
|
|
186
|
+
# apart from "measured, and it is empty".
|
|
187
|
+
return 1 if missing_savepoint(dir)
|
|
188
|
+
|
|
189
|
+
record = GraphMeasure.read(dir)
|
|
190
|
+
puts format == "json" ? GraphMeasureReport.render_json(record) : GraphMeasureReport.render_text(record)
|
|
191
|
+
0
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def run_budget(dir, format)
|
|
195
|
+
return 1 if missing_savepoint(dir)
|
|
196
|
+
|
|
197
|
+
record = GraphMeasureBudget.read(dir)
|
|
198
|
+
puts format == "json" ? GraphMeasureBudget.render_json(record) : GraphMeasureBudget.render_text(record)
|
|
199
|
+
0
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Row 2.5's "missing savepoint.md" check does not apply here: `dir` is a
|
|
203
|
+
# STORE directory (spec D1's "the directory holding id--slug/ intent
|
|
204
|
+
# folders"), which never carries a savepoint.md of its own.
|
|
205
|
+
#
|
|
206
|
+
# n6 adds the rates, the latency and the bar to this same verb, sourced
|
|
207
|
+
# from `GraphMeasureCohorts`. Its bar section reuses the one
|
|
208
|
+
# `GraphMeasureModels` walk's `qualified` counts (passed in here) rather
|
|
209
|
+
# than triggering a second store walk for the same numbers.
|
|
210
|
+
#
|
|
211
|
+
# M1 (v1 review): this call used to pass neither `project_config:` nor
|
|
212
|
+
# `global_config:`, so the model comparison always resolved against
|
|
213
|
+
# RunnerPolicy's shipped defaults only, never the real project-then-
|
|
214
|
+
# global-then-default chain the CHANGELOG describes and
|
|
215
|
+
# `scripts/doctor.rb`'s own `model_requalification_checks` actually reads.
|
|
216
|
+
# Reproduced: with a `plastic-executor:` override present in global
|
|
217
|
+
# config, `GraphMeasureModels.read` reports real drift rows while this
|
|
218
|
+
# verb (unpatched) always reported zero. `GraphMeasure`'s own modules stay
|
|
219
|
+
# config-free by design (D2's DI convention: they never read disk or ENV
|
|
220
|
+
# themselves) - this executable is the boundary that is allowed to, the
|
|
221
|
+
# same role `scripts/doctor.rb` already plays for the same two config
|
|
222
|
+
# files.
|
|
223
|
+
#
|
|
224
|
+
# NEW-4 (v2 review, D22): the project-scope half named `<store>/../project.yml`,
|
|
225
|
+
# a file whose template carries no `agents:` key at all, so a real
|
|
226
|
+
# operator override at the file that DOES exist in the wild - the store's
|
|
227
|
+
# sibling `config.yml` - was silently ignored. The path now comes from
|
|
228
|
+
# `GraphMeasureModels.project_config_path`, the one function
|
|
229
|
+
# `scripts/doctor.rb`'s own rule calls too, so the two commands cannot
|
|
230
|
+
# resolve a different file for the same store.
|
|
231
|
+
def run_cohorts(dir, format)
|
|
232
|
+
global_config = load_yaml_safe(File.join(plastic_home, "config.yml"))
|
|
233
|
+
project_config = load_yaml_safe(GraphMeasureModels.project_config_path(dir))
|
|
234
|
+
|
|
235
|
+
models_record = GraphMeasureModels.read(dir, project_config: project_config, global_config: global_config)
|
|
236
|
+
cohorts_record = GraphMeasureCohorts.read(dir, qualified: models_record[:qualified],
|
|
237
|
+
project_config: project_config, global_config: global_config)
|
|
238
|
+
if format == "json"
|
|
239
|
+
combined = GraphMeasureModels.model(models_record).merge(GraphMeasureCohorts.model(cohorts_record))
|
|
240
|
+
puts JSON.generate(combined)
|
|
241
|
+
else
|
|
242
|
+
puts GraphMeasureModels.render_text(models_record)
|
|
243
|
+
puts GraphMeasureCohorts.render_text(cohorts_record)
|
|
244
|
+
end
|
|
245
|
+
0
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
exit GraphMeasureCLI.main(ARGV) if $PROGRAM_NAME == __FILE__
|
|
@@ -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 = {
|