@zalom/plastic 2.0.0-alpha.21 → 2.0.0-alpha.23

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.
Files changed (86) hide show
  1. package/PLASTIC.md +20 -18
  2. package/agents/plastic-enforcer.md +6 -3
  3. package/agents/plastic-executor.md +4 -0
  4. package/agents/plastic-node-research.md +28 -0
  5. package/agents/plastic-node-verify.md +27 -0
  6. package/agents/plastic-node-work.md +32 -0
  7. package/bin/lib/context_budget.rb +1 -1
  8. package/hooks/hooks.json +12 -0
  9. package/hooks/statusline +28 -0
  10. package/hooks/stop +5 -0
  11. package/package.json +1 -1
  12. package/scripts/doctor.rb +159 -10
  13. package/scripts/end-intent +3 -3
  14. package/scripts/graph-measure +249 -0
  15. package/scripts/hook-capture +1 -0
  16. package/scripts/hook-savepoint +24 -2
  17. package/scripts/hook-session-start +333 -321
  18. package/scripts/hook-stop +57 -0
  19. package/scripts/insight-append +18 -4
  20. package/scripts/lib/active_delivery.rb +61 -0
  21. package/scripts/lib/agent_models.rb +10 -1
  22. package/scripts/lib/codex_adapter.rb +197 -0
  23. package/scripts/lib/doctor_core.rb +8 -3
  24. package/scripts/lib/engine_permissions.rb +88 -0
  25. package/scripts/lib/graph_edges.rb +4 -4
  26. package/scripts/lib/graph_file.rb +4 -4
  27. package/scripts/lib/graph_measure.rb +645 -0
  28. package/scripts/lib/graph_measure_budget.rb +408 -0
  29. package/scripts/lib/graph_measure_cohorts.rb +487 -0
  30. package/scripts/lib/graph_measure_models.rb +411 -0
  31. package/scripts/lib/graph_measure_report.rb +532 -0
  32. package/scripts/lib/graph_tree.rb +2 -2
  33. package/scripts/lib/handoff.rb +36 -5
  34. package/scripts/lib/harness_adapter.rb +184 -0
  35. package/scripts/lib/hook_registry.rb +13 -1
  36. package/scripts/lib/hook_replay.rb +23 -5
  37. package/scripts/lib/index_projection.rb +1 -1
  38. package/scripts/lib/installer_core.rb +112 -6
  39. package/scripts/lib/intent_screen.rb +1 -1
  40. package/scripts/lib/intent_validator.rb +2 -2
  41. package/scripts/lib/meter_watch.rb +15 -9
  42. package/scripts/lib/node_file.rb +3 -3
  43. package/scripts/lib/node_ledger.rb +8 -1
  44. package/scripts/lib/node_progress.rb +153 -0
  45. package/scripts/lib/outcome_report.rb +1 -1
  46. package/scripts/lib/report_screen.rb +10 -6
  47. package/scripts/lib/roadmap_graph.rb +1 -1
  48. package/scripts/lib/roadmap_queue.rb +1 -1
  49. package/scripts/lib/roadmap_render.rb +1 -1
  50. package/scripts/lib/runner_absorb.rb +31 -5
  51. package/scripts/lib/runner_dispatch.rb +26 -11
  52. package/scripts/lib/runner_until_empty.rb +252 -0
  53. package/scripts/lib/runner_watch.rb +389 -0
  54. package/scripts/lib/savepoint.rb +3 -3
  55. package/scripts/lib/session_git.rb +2 -2
  56. package/scripts/lib/stop_gate.rb +95 -0
  57. package/scripts/lib/verify_intent.rb +2 -2
  58. package/scripts/lib/work_graph_validator.rb +6 -6
  59. package/scripts/new-intent +1 -1
  60. package/scripts/node-run +224 -0
  61. package/scripts/read-config +6 -0
  62. package/scripts/runner +203 -19
  63. package/scripts/skill-lint +115 -6
  64. package/scripts/verify-intent +1 -1
  65. package/skills/auto/SKILL.md +54 -56
  66. package/skills/auto/references/agent-architecture.md +10 -8
  67. package/skills/auto/references/human-report-contract.md +1 -1
  68. package/skills/conventions/references/completion-and-done.md +7 -7
  69. package/skills/conventions/references/knowledge-graph.md +9 -0
  70. package/skills/conventions/references/locks-and-worktrees.md +3 -3
  71. package/skills/conventions/references/maintenance-and-revisions.md +1 -1
  72. package/skills/doctor/SKILL.md +3 -3
  73. package/skills/doctor/report.md +1 -1
  74. package/skills/intent-continuing/references/boarding-matrix.md +2 -2
  75. package/skills/intent-creating/SKILL.md +58 -133
  76. package/skills/intent-creating/evals/evals.json +1 -1
  77. package/skills/intent-ending/SKILL.md +48 -56
  78. package/skills/intent-ending/evals/evals.json +1 -1
  79. package/skills/intent-executing/SKILL.md +43 -136
  80. package/skills/intent-speccing/SKILL.md +3 -0
  81. package/skills/releasing/SKILL.md +1 -1
  82. package/skills/releasing/references/release-lines.md +1 -1
  83. package/skills/tutorial/SKILL.md +2 -1
  84. package/skills/tutorial/references/track-1-guided.md +21 -40
  85. package/skills/tutorial/references/track-2-auto.md +3 -3
  86. package/templates/agents.md +2 -2
@@ -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__
@@ -11,6 +11,7 @@
11
11
 
12
12
  require "json"
13
13
  require "open3"
14
+ require "time"
14
15
  require "fileutils"
15
16
  require_relative "lib/session_ledger"
16
17
  require_relative "lib/dashboard_banner"
@@ -15,12 +15,31 @@
15
15
  require "json"
16
16
  require_relative "lib/session_ledger"
17
17
  require_relative "lib/handoff"
18
+ require_relative "lib/active_delivery"
18
19
 
19
20
  MESSAGE = "PLASTIC SAVEPOINT - context is being compacted. The hand-off for this " \
20
21
  "session is written in today's day ledger " \
21
22
  "(store/.sessions/<day>/handoff--<session>.md). " \
22
23
  "After compaction say continue to resume from it."
23
24
 
25
+ # The intent this session is delivering, resolved through the same walk the
26
+ # Stop gate uses (row 4.38): read-config's project_roots, plus the global
27
+ # store, glob for a live delivery.lock this session holds. Never raises
28
+ # (ActiveDelivery.resolve rescues its own reads); a lookup failure just
29
+ # means the Runner section is omitted, never that the hand-off is lost.
30
+ def resolve_intent(plastic_home, session_id, read_config_path)
31
+ return nil if session_id.strip.empty?
32
+
33
+ global_store = File.join(plastic_home, "store")
34
+ roots_raw = IO.popen({ "PLASTIC_HOME" => plastic_home }, [read_config_path, "project_roots"],
35
+ err: File::NULL, &:read).to_s.strip
36
+ project_roots = roots_raw.empty? ? [] : Array(JSON.parse(roots_raw))
37
+ project_roots = project_roots.map { |root| File.expand_path(root.to_s) }
38
+ ActiveDelivery.resolve(global_store: global_store, project_roots: project_roots, session: session_id)
39
+ rescue StandardError
40
+ nil
41
+ end
42
+
24
43
  written = nil
25
44
  begin
26
45
  plastic_home = ARGV[0].to_s
@@ -30,11 +49,14 @@ begin
30
49
  session_id = payload["session_id"].to_s
31
50
 
32
51
  unless plastic_home.empty? || session_id.strip.empty?
33
- store = File.join(File.expand_path(plastic_home), "store")
52
+ home = File.expand_path(plastic_home)
53
+ store = File.join(home, "store")
34
54
  session = SessionLedger.short_session_id(session_id, nil)
35
55
  day = Handoff.day_for(store, session, today: SessionLedger.day_id)
56
+ read_config_path = File.expand_path("read-config", __dir__)
57
+ intent = resolve_intent(home, session_id, read_config_path)
36
58
  written = Handoff.write(store: store, day: day, session: session, trigger: "precompact",
37
- templates: File.expand_path("../templates", __dir__))
59
+ templates: File.expand_path("../templates", __dir__), intent: intent)
38
60
  end
39
61
  rescue StandardError, JSON::ParserError
40
62
  nil