@zalom/plastic 2.0.0-alpha.27 → 2.0.0-alpha.28

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 (207) hide show
  1. package/PLASTIC.md +13 -139
  2. package/README.md +337 -133
  3. package/agents/plastic-enforcer.md +9 -10
  4. package/agents/plastic-executor.md +1 -1
  5. package/bin/crap +4 -0
  6. package/bin/lib/context_budget.rb +35 -1
  7. package/bin/lib/skill_census.rb +839 -0
  8. package/bin/plastic +6 -0
  9. package/bin/plastic-skill-census +114 -0
  10. package/bin/verify-change +345 -0
  11. package/deprecations.yml +1 -1
  12. package/{skills/agent-advisor/references → docs/help}/advisor-protocol.md +4 -7
  13. package/{skills/auto/references → docs/help}/agent-architecture.md +7 -7
  14. package/{skills/conventions/references → docs/help}/completion-and-done.md +1 -1
  15. package/{skills/auto/references → docs/help}/human-report-contract.md +17 -19
  16. package/{skills/conventions/references → docs/help}/roadmaps.md +2 -2
  17. package/{skills/tutorial/references → docs/help}/track-1-guided.md +8 -8
  18. package/{skills/tutorial/references → docs/help}/track-2-auto.md +5 -5
  19. package/{skills/tutorial/references → docs/help}/track-3-projects-and-roadmaps.md +24 -17
  20. package/package.json +3 -2
  21. package/scripts/append-ledger +2 -1
  22. package/scripts/dashboard.rb +10 -9
  23. package/scripts/day-summary +2 -1
  24. package/scripts/doctor.rb +48 -42
  25. package/scripts/end-intent +2 -8
  26. package/scripts/file-session-intent +2 -1
  27. package/scripts/hook-capture +4 -3
  28. package/scripts/hook-close +2 -1
  29. package/scripts/hook-record +3 -2
  30. package/scripts/hook-savepoint +3 -2
  31. package/scripts/hook-session-start +5 -4
  32. package/scripts/hook-stop +2 -1
  33. package/scripts/insight-append +1 -2
  34. package/scripts/install.rb +3 -1
  35. package/scripts/lib/active_delivery.rb +1 -1
  36. package/scripts/lib/arm.rb +2 -1
  37. package/scripts/lib/backup.rb +63 -0
  38. package/scripts/lib/cli/command.rb +85 -0
  39. package/scripts/lib/cli/commands/auto.rb +18 -0
  40. package/scripts/lib/cli/commands/auto_brief.rb +44 -0
  41. package/scripts/lib/cli/commands/auto_lock.rb +60 -0
  42. package/scripts/lib/cli/commands/auto_report.rb +50 -0
  43. package/scripts/lib/cli/commands/auto_take.rb +25 -0
  44. package/scripts/lib/cli/commands/backup.rb +43 -0
  45. package/scripts/lib/cli/commands/checkout.rb +25 -0
  46. package/scripts/lib/cli/commands/continue.rb +66 -0
  47. package/scripts/lib/cli/commands/doctor.rb +20 -0
  48. package/scripts/lib/cli/commands/feedback.rb +40 -0
  49. package/scripts/lib/cli/commands/help.rb +69 -0
  50. package/scripts/lib/cli/commands/hook.rb +32 -0
  51. package/scripts/lib/cli/commands/index.rb +23 -0
  52. package/scripts/lib/cli/commands/install.rb +21 -0
  53. package/scripts/lib/cli/commands/installer_verb.rb +37 -0
  54. package/scripts/lib/cli/commands/intent.rb +19 -0
  55. package/scripts/lib/cli/commands/intent_answer.rb +37 -0
  56. package/scripts/lib/cli/commands/intent_command.rb +53 -0
  57. package/scripts/lib/cli/commands/intent_end.rb +61 -0
  58. package/scripts/lib/cli/commands/intent_new.rb +62 -0
  59. package/scripts/lib/cli/commands/intent_note.rb +43 -0
  60. package/scripts/lib/cli/commands/intent_rule.rb +36 -0
  61. package/scripts/lib/cli/commands/intent_show.rb +25 -0
  62. package/scripts/lib/cli/commands/intent_spec.rb +44 -0
  63. package/scripts/lib/cli/commands/intent_step.rb +43 -0
  64. package/scripts/lib/cli/commands/intent_verify.rb +26 -0
  65. package/scripts/lib/cli/commands/migrate.rb +16 -0
  66. package/scripts/lib/cli/commands/migrate_stores.rb +31 -0
  67. package/scripts/lib/cli/commands/next.rb +49 -0
  68. package/scripts/lib/cli/commands/project.rb +19 -0
  69. package/scripts/lib/cli/commands/project_links.rb +42 -0
  70. package/scripts/lib/cli/commands/project_list.rb +20 -0
  71. package/scripts/lib/cli/commands/project_new.rb +72 -0
  72. package/scripts/lib/cli/commands/query.rb +31 -0
  73. package/scripts/lib/cli/commands/render.rb +25 -0
  74. package/scripts/lib/cli/commands/roadmap.rb +19 -0
  75. package/scripts/lib/cli/commands/roadmap_check.rb +44 -0
  76. package/scripts/lib/cli/commands/roadmap_log.rb +54 -0
  77. package/scripts/lib/cli/commands/roadmap_next.rb +34 -0
  78. package/scripts/lib/cli/commands/roadmap_show.rb +45 -0
  79. package/scripts/lib/cli/commands/rollback.rb +20 -0
  80. package/scripts/lib/cli/commands/search.rb +60 -0
  81. package/scripts/lib/cli/commands/session.rb +18 -0
  82. package/scripts/lib/cli/commands/session_commit.rb +42 -0
  83. package/scripts/lib/cli/commands/session_handoff.rb +34 -0
  84. package/scripts/lib/cli/commands/session_summary.rb +35 -0
  85. package/scripts/lib/cli/commands/status.rb +68 -0
  86. package/scripts/lib/cli/commands/subcommand_list.rb +36 -0
  87. package/scripts/lib/cli/commands/sync.rb +46 -0
  88. package/scripts/lib/cli/commands/uninstall.rb +20 -0
  89. package/scripts/lib/cli/commands/update.rb +20 -0
  90. package/scripts/lib/cli/commands/version.rb +53 -0
  91. package/scripts/lib/cli/frontier.rb +84 -0
  92. package/scripts/lib/cli/legacy.rb +50 -0
  93. package/scripts/lib/cli/output.rb +102 -0
  94. package/scripts/lib/cli/scope.rb +127 -0
  95. package/scripts/lib/cli/table.rb +64 -0
  96. package/scripts/lib/cli.rb +94 -0
  97. package/scripts/lib/compact_instructions.rb +8 -0
  98. package/scripts/lib/day_summary.rb +4 -3
  99. package/scripts/lib/doctor_core.rb +7 -32
  100. package/scripts/lib/doctor_session_ledger.rb +2 -1
  101. package/scripts/lib/feedback_report.rb +1 -1
  102. package/scripts/lib/graph_measure_models.rb +3 -1
  103. package/scripts/lib/index_entry.rb +9 -0
  104. package/scripts/lib/installer_core.rb +73 -19
  105. package/scripts/lib/intent_screen.rb +3 -3
  106. package/scripts/lib/lock.rb +2 -2
  107. package/scripts/lib/node_input.rb +3 -2
  108. package/scripts/lib/preflight.rb +2 -4
  109. package/scripts/lib/project_config.rb +2 -1
  110. package/scripts/lib/project_validator.rb +3 -2
  111. package/scripts/lib/qmd_sync.rb +8 -7
  112. package/scripts/lib/reference_archive.rb +45 -0
  113. package/scripts/lib/release_guard.rb +2 -0
  114. package/scripts/lib/report_screen.rb +4 -3
  115. package/scripts/lib/rlm/corpus.rb +13 -0
  116. package/scripts/lib/rlm/probe.rb +29 -0
  117. package/scripts/lib/rlm/query.rb +22 -0
  118. package/scripts/lib/roadmap_queue.rb +2 -2
  119. package/scripts/lib/roadmap_savepoint.rb +1 -1
  120. package/scripts/lib/runner_absorb.rb +3 -2
  121. package/scripts/lib/search_index.rb +55 -0
  122. package/scripts/lib/session_git.rb +4 -3
  123. package/scripts/lib/sqlite.rb +22 -0
  124. package/scripts/lib/store_discovery.rb +7 -6
  125. package/scripts/lib/store_layout.rb +54 -0
  126. package/scripts/lib/store_provisioning.rb +2 -1
  127. package/scripts/lib/store_sync.rb +85 -0
  128. package/scripts/lib/stores_move.rb +93 -0
  129. package/scripts/lib/verify_intent.rb +2 -7
  130. package/scripts/lib/version_number.rb +48 -0
  131. package/scripts/lib/work_graph.rb +59 -0
  132. package/scripts/lib/worktree.rb +3 -8
  133. package/scripts/lib/worktree_sweep.rb +3 -2
  134. package/scripts/link-suggest +2 -1
  135. package/scripts/migrate-to-global +1 -1
  136. package/scripts/new-intent +3 -12
  137. package/scripts/plastic-lock +3 -2
  138. package/scripts/promote-session-item +3 -2
  139. package/scripts/release-check +10 -5
  140. package/scripts/report-screen +1 -1
  141. package/scripts/session-commit +2 -1
  142. package/scripts/spawn-preamble +2 -2
  143. package/scripts/update.rb +25 -4
  144. package/scripts/write-handoff +2 -1
  145. package/templates/agents.md +6 -6
  146. package/templates/render.css +10 -0
  147. package/bin/plastic.js +0 -70
  148. package/skills/agent-advisor/SKILL.md +0 -84
  149. package/skills/auto/SKILL.md +0 -297
  150. package/skills/auto/evals/evals.json +0 -255
  151. package/skills/auto/references/end-tail.md +0 -64
  152. package/skills/conventions/SKILL.md +0 -29
  153. package/skills/dashboard/SKILL.md +0 -180
  154. package/skills/dashboard/evals/evals.json +0 -38
  155. package/skills/dashboard/references/classification.md +0 -22
  156. package/skills/dashboard/templates/dashboard-global.md +0 -20
  157. package/skills/dashboard/templates/dashboard-project.md +0 -19
  158. package/skills/direct/SKILL.md +0 -66
  159. package/skills/direct/references/request-signals.md +0 -59
  160. package/skills/doctor/SKILL.md +0 -305
  161. package/skills/doctor/report.md +0 -102
  162. package/skills/feedback/SKILL.md +0 -98
  163. package/skills/feedback/references/transport-and-privacy.md +0 -65
  164. package/skills/feedback/report.md +0 -36
  165. package/skills/install/SKILL.md +0 -215
  166. package/skills/intent-continuing/SKILL.md +0 -156
  167. package/skills/intent-continuing/references/board-fill.md +0 -52
  168. package/skills/intent-continuing/references/boarding-matrix.md +0 -35
  169. package/skills/intent-continuing/references/context-management.md +0 -28
  170. package/skills/intent-continuing/references/liveness-ranking.md +0 -57
  171. package/skills/intent-creating/SKILL.md +0 -89
  172. package/skills/intent-creating/evals/evals.json +0 -72
  173. package/skills/intent-creating/references/lifecycle.md +0 -81
  174. package/skills/intent-creating/references/wikilinks.md +0 -8
  175. package/skills/intent-ending/SKILL.md +0 -182
  176. package/skills/intent-ending/evals/evals.json +0 -74
  177. package/skills/intent-executing/SKILL.md +0 -87
  178. package/skills/intent-executing/evals/evals.json +0 -66
  179. package/skills/intent-executing/implementer-prompt.md +0 -47
  180. package/skills/intent-executing/spec-reviewer-prompt.md +0 -27
  181. package/skills/intent-speccing/SKILL.md +0 -136
  182. package/skills/intent-speccing/evals/evals.json +0 -126
  183. package/skills/intent-speccing/references/design-principles.md +0 -44
  184. package/skills/intent-speccing/references/per-section-fill-rules.md +0 -92
  185. package/skills/intent-speccing/references/self-verify-checklist.md +0 -37
  186. package/skills/project-creating/SKILL.md +0 -162
  187. package/skills/project-creating/references/hubs-projects.md +0 -55
  188. package/skills/project-creating/references/project-scaffolding.md +0 -97
  189. package/skills/releasing/SKILL.md +0 -376
  190. package/skills/releasing/references/deprecations.md +0 -60
  191. package/skills/releasing/references/promotion-and-tagging.md +0 -70
  192. package/skills/releasing/references/release-lines.md +0 -105
  193. package/skills/roadmap/SKILL.md +0 -90
  194. package/skills/roadmap/references/file-format.md +0 -134
  195. package/skills/roadmap/references/operations.md +0 -112
  196. package/skills/rollback/SKILL.md +0 -91
  197. package/skills/tutorial/SKILL.md +0 -66
  198. package/skills/tutorial/evals/evals.json +0 -186
  199. package/skills/uninstall/SKILL.md +0 -75
  200. package/skills/update/SKILL.md +0 -126
  201. /package/{skills/auto/references → docs/help}/agent-report-contract.md +0 -0
  202. /package/{skills/intent-executing → docs/help}/code-quality-reviewer-prompt.md +0 -0
  203. /package/{skills/conventions/references → docs/help}/knowledge-graph.md +0 -0
  204. /package/{skills/conventions/references → docs/help}/lifecycle-and-savepoints.md +0 -0
  205. /package/{skills/conventions/references → docs/help}/locks-and-worktrees.md +0 -0
  206. /package/{skills/conventions/references → docs/help}/maintenance-and-revisions.md +0 -0
  207. /package/{skills/intent-executing → docs/help}/plan-reviewer-prompt.md +0 -0
package/bin/plastic ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env -S ruby --disable-gems
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../scripts/lib/cli"
5
+
6
+ exit Plastic::CLI.call(ARGV)
@@ -0,0 +1,114 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: UTF-8
3
+ # frozen_string_literal: true
4
+
5
+ # Plastic skill usage census (intent 324). Counts typed /plastic-* commands
6
+ # from the prompt history against agent-invoked Skill calls, loads, and
7
+ # attribution from the transcript tree, so D19 (which Plastic verbs stay
8
+ # visible as slash commands) is decided on measured use.
9
+ #
10
+ # Maintainer tool: it reads the paths given to it and is never installed
11
+ # into ~/.plastic. This file never resolves the running process's home
12
+ # directory or its home environment variable: `bin/` ships in the npm
13
+ # tarball, so a default rooted there would make a published executable
14
+ # read any installer's transcripts. `--history` and `--transcripts` are
15
+ # required, with no default.
16
+ #
17
+ # Usage: bin/plastic-skill-census --history PATH --transcripts DIR
18
+ # [--skills-dir DIR] [--format md|json] [--out PATH]
19
+ # [--cutoff YYYY-MM-DD]
20
+ #
21
+ # --history PATH ~/.claude/history.jsonl, or a fixture (required)
22
+ # --transcripts DIR ~/.claude/projects, or a fixture tree (required)
23
+ # --skills-dir DIR a skill roster directory (default: this repo's skills/)
24
+ # --format md|json report format (default: md)
25
+ # --out PATH write the report here instead of stdout
26
+ # --cutoff YYYY-MM-DD exclude records on or after this date (default: 2026-09-02)
27
+ #
28
+ # Exit codes: 0 a report was produced, 1 a named path does not exist,
29
+ # 2 bad usage or a missing required flag.
30
+
31
+ require_relative "lib/skill_census"
32
+
33
+ USAGE = <<~TEXT
34
+ usage: plastic-skill-census --history PATH --transcripts DIR [--skills-dir DIR] [--format md|json] [--out PATH] [--cutoff YYYY-MM-DD]
35
+
36
+ --history PATH ~/.claude/history.jsonl, or a fixture (required)
37
+ --transcripts DIR ~/.claude/projects, or a fixture tree (required)
38
+ --skills-dir DIR a skill roster directory (default: this repo's skills/)
39
+ --format md|json report format (default: md)
40
+ --out PATH write the report here instead of stdout
41
+ --cutoff YYYY-MM-DD exclude records on or after this date (default: #{SkillCensus::DEFAULT_CUTOFF})
42
+ --help this message
43
+
44
+ Exits 0 on a report, 1 when a named path is missing, 2 on bad usage.
45
+ TEXT
46
+
47
+ def fail_usage(message)
48
+ warn "plastic-skill-census: #{message}"
49
+ warn USAGE
50
+ exit 2
51
+ end
52
+
53
+ history = nil
54
+ transcripts = nil
55
+ skills_dir = File.expand_path("../skills", __dir__)
56
+ format = "md"
57
+ out_path = nil
58
+ cutoff = SkillCensus::DEFAULT_CUTOFF
59
+
60
+ argv = ARGV.dup
61
+ until argv.empty?
62
+ case (flag = argv.shift)
63
+ when "--help", "-h"
64
+ puts USAGE
65
+ exit 0
66
+ when "--history"
67
+ history = argv.shift
68
+ fail_usage("--history needs a path") if history.to_s.empty?
69
+ when "--transcripts"
70
+ transcripts = argv.shift
71
+ fail_usage("--transcripts needs a path") if transcripts.to_s.empty?
72
+ when "--skills-dir"
73
+ skills_dir = argv.shift
74
+ fail_usage("--skills-dir needs a path") if skills_dir.to_s.empty?
75
+ when "--format"
76
+ format = argv.shift
77
+ fail_usage("--format must be md or json") unless %w[md json].include?(format)
78
+ when "--out"
79
+ out_path = argv.shift
80
+ fail_usage("--out needs a path") if out_path.to_s.empty?
81
+ when "--cutoff"
82
+ cutoff = argv.shift
83
+ fail_usage("--cutoff needs a YYYY-MM-DD date") if cutoff.to_s.empty?
84
+ else
85
+ fail_usage("unknown argument #{flag}")
86
+ end
87
+ end
88
+
89
+ fail_usage("--history is required") if history.nil?
90
+ fail_usage("--transcripts is required") if transcripts.nil?
91
+
92
+ begin
93
+ roster = SkillCensus::Roster.load(skills_dir)
94
+ history_scan = SkillCensus::HistoryScanner.new(history, cutoff: cutoff).scan
95
+ transcript_scan = SkillCensus::TranscriptScanner.new(transcripts, cutoff: cutoff).scan
96
+ built = SkillCensus::Tally.new(history_scan, transcript_scan, roster).build
97
+
98
+ report = if format == "json"
99
+ SkillCensus::Report.json(built, history_path: history, transcripts_path: transcripts, cutoff: cutoff)
100
+ else
101
+ SkillCensus::Report.markdown(built, history_path: history, transcripts_path: transcripts, cutoff: cutoff)
102
+ end
103
+ rescue RuntimeError => error
104
+ warn "plastic-skill-census: #{error.message}"
105
+ exit 1
106
+ end
107
+
108
+ if out_path
109
+ File.write(out_path, report)
110
+ else
111
+ puts report
112
+ end
113
+
114
+ exit 0
@@ -0,0 +1,345 @@
1
+ #!/usr/bin/env ruby
2
+ require "fileutils"
3
+ require "open3"
4
+ require "tmpdir"
5
+
6
+ class VerifyChange
7
+ USAGE = <<~TEXT
8
+ Usage: bin/verify-change [BASE] [--test FILE]...
9
+
10
+ Checks only the code changed since the merge base of BASE and HEAD
11
+ (BASE defaults to origin/main, then main, then master):
12
+
13
+ 1. lint on the changed Ruby files, in the project's style guide
14
+ 2. the tests for the changed files, plus every --test FILE, with coverage on
15
+ 3. patch coverage of the changed lines (simplecov patch)
16
+ 4. mutation testing of the changed lines (mutineer, operators from .mutineer.yml)
17
+ 5. CRAP scores of the changed methods (bin/crap)
18
+
19
+ Runs RSpec specs in a project with spec/ and .rspec, spec/spec_helper.rb,
20
+ or spec/rails_helper.rb, or with rspec, rspec-core, or rspec-rails under
21
+ DEPENDENCIES in Gemfile.lock, and Minitest tests otherwise. Warns when
22
+ both test/ and spec/ exist. Pass --test for the integration or system
23
+ tests of a feature the change touches.
24
+
25
+ A lint failure is reported but does not stop the other checks. Lint is
26
+ skipped, not failed, with no style guide, two style guides, no changed
27
+ Ruby files, or the linter gem missing from Gemfile.lock.
28
+
29
+ Every child runs under a throwaway HOME and PLASTIC_TMP, so a test or a
30
+ mutant that spawns an installer cannot reach the real home directory.
31
+
32
+ Exit codes: 0 verified, 1 a check failed, 2 usage or git error.
33
+ TEXT
34
+
35
+ PATCH_COVERAGE_MINIMUM = 100
36
+ MUTATION_THRESHOLD = 75
37
+ CRAP_THRESHOLD = 30
38
+ TESTS_STEP = "Tests with coverage"
39
+ LINT_STEP = "Lint"
40
+ DEFAULT_BASES = %w[origin/main main master].freeze
41
+ ALL_OPERATORS = %w[arithmetic comparison boolean_connector boolean_literal statement_removal return_nil
42
+ literal_mutation condition_negation string_literal regex collection_method].freeze
43
+ SOURCE_PATH = %r{\A(app|lib|tools|scripts|bin/lib)/.+\.rb\z}
44
+ RSPEC_DEPENDENCY = /^ rspec(-core|-rails)?(\s|!|$)/
45
+
46
+ Suite = Data.define(:name, :folder, :test_path, :locations, :suffix)
47
+ MINITEST = Suite.new(name: "Minitest", folder: "test/", test_path: %r{\Atest/.+_test\.rb\z}, suffix: "_test.rb",
48
+ locations: { %r{\Aapp/} => ["test/"], %r{\Alib/} => ["test/", "test/lib/"], %r{\Atools/} => ["test/tools/"],
49
+ %r{\Ascripts/lib/} => ["test/", "test/lib/"], %r{\Abin/lib/} => ["test/"], %r{\Ascripts/} => ["test/"] })
50
+ RSPEC = Suite.new(name: "RSpec", folder: "spec/", test_path: %r{\Aspec/.+_spec\.rb\z}, suffix: "_spec.rb",
51
+ locations: { %r{\Aapp/} => ["spec/"], %r{\Alib/} => ["spec/", "spec/lib/"], %r{\Atools/} => ["spec/tools/"] })
52
+
53
+ Step = Struct.new(:title, :env, :command)
54
+ LintDecision = Struct.new(:step, :skip_reason)
55
+ Plan = Struct.new(:sources, :tests, :untested, :lint_files, :lint_decision, :steps)
56
+
57
+ def self.test_candidates(source, suite = MINITEST)
58
+ suite.locations.flat_map do |prefix, targets|
59
+ source.match?(prefix) ? targets.map { |target| source.sub(prefix, target).sub(/\.rb\z/, suite.suffix) } : []
60
+ end
61
+ end
62
+
63
+ def initialize(argv, root: Dir.pwd, out: $stdout, err: $stderr, git: nil, run: nil, sandbox: nil)
64
+ @argv = argv.dup
65
+ @root = root
66
+ @out = out
67
+ @err = err
68
+ @git = git || ->(*args) { Open3.capture2e("git", "-C", root, *args) }
69
+ @run = run || ->(env, command) { system(env, *command, chdir: root) }
70
+ @sandbox = sandbox
71
+ @owned_sandbox = sandbox.nil?
72
+ end
73
+
74
+ # A throwaway home for every child this script spawns. A gate step runs the
75
+ # project's own tests, and a test (or a mutant that defeats an injected
76
+ # runner) can spawn an installer, which resolves ~/.plastic and ~/.claude
77
+ # from HOME. Pointing HOME at a fresh directory is what keeps the real home
78
+ # out of reach; PLASTIC_TMP does the same for the session bridge files.
79
+ def sandbox
80
+ @sandbox ||= { "HOME" => Dir.mktmpdir("verify-change-home"),
81
+ "PLASTIC_TMP" => Dir.mktmpdir("verify-change-tmp") }
82
+ end
83
+
84
+ def call
85
+ return usage(0) if @argv.include?("--help") || @argv.include?("-h")
86
+
87
+ requested_base, extra_tests = parse_arguments
88
+ return usage(2) unless requested_base
89
+
90
+ base = merge_base(requested_base)
91
+ return 2 unless base
92
+
93
+ report_untracked
94
+ chosen = suite
95
+ report_both_suites(chosen)
96
+ plan = plan(changed: changed_files(base), extra_tests: extra_tests, base: base, rails: rails?, suite: chosen)
97
+ execute(plan, requested_base)
98
+ ensure
99
+ discard_sandbox
100
+ end
101
+
102
+ def discard_sandbox
103
+ return unless @owned_sandbox && @sandbox
104
+
105
+ @sandbox.each_value { |dir| FileUtils.remove_entry(dir, true) }
106
+ @sandbox = nil
107
+ end
108
+
109
+ def plan(changed:, extra_tests:, base:, rails:, suite: MINITEST)
110
+ sources = changed.grep(SOURCE_PATH).select { |path| exist?(path) }
111
+ lint_files = changed.select { |path| File.extname(path) == ".rb" && exist?(path) }
112
+ mapped = sources.to_h { |source| [source, self.class.test_candidates(source, suite).select { |path| exist?(path) }] }
113
+ changed_tests = changed.grep(suite.test_path)
114
+ tests = (changed_tests + mapped.values.flatten + extra_tests).uniq.select { |path| exist?(path) }
115
+ covered_elsewhere = !extra_tests.empty? || changed_tests.any?
116
+ untested = covered_elsewhere ? [] : mapped.select { |_source, found| found.empty? }.keys
117
+ Plan.new(sources, tests, untested, lint_files, lint_decision(lint_files), steps(sources, tests, base, rails, suite))
118
+ end
119
+
120
+ private
121
+
122
+ def parse_arguments
123
+ base = nil
124
+ extra_tests = []
125
+ arguments = @argv.dup
126
+ until arguments.empty?
127
+ argument = arguments.shift
128
+ if argument == "--test"
129
+ file = arguments.shift
130
+ return [nil, []] unless file
131
+
132
+ extra_tests << file
133
+ elsif argument.start_with?("-") || base
134
+ return [nil, []]
135
+ else
136
+ base = argument
137
+ end
138
+ end
139
+ [base || default_base, extra_tests]
140
+ end
141
+
142
+ def default_base
143
+ DEFAULT_BASES.find { |ref| @git.call("rev-parse", "--verify", "--quiet", ref).last.success? } || DEFAULT_BASES.last
144
+ end
145
+
146
+ def merge_base(ref)
147
+ output, status = @git.call("merge-base", ref, "HEAD")
148
+ return output.strip if status.success?
149
+
150
+ @err.puts "Cannot find the merge base of #{ref} and HEAD: #{output.strip}"
151
+ nil
152
+ end
153
+
154
+ def changed_files(base)
155
+ @git.call("diff", "--name-only", base, "--", "*.rb").first.split("\n")
156
+ end
157
+
158
+ def report_untracked
159
+ untracked = @git.call("ls-files", "--others", "--exclude-standard", "--", "*.rb").first.split("\n")
160
+ return if untracked.empty?
161
+
162
+ @err.puts "Not checked, because git does not track them yet (run git add first): #{untracked.join(", ")}"
163
+ end
164
+
165
+ def report_both_suites(chosen)
166
+ return unless File.directory?(File.join(@root, "test")) && File.directory?(File.join(@root, "spec"))
167
+
168
+ skipped = chosen == RSPEC ? MINITEST : RSPEC
169
+ @err.puts "Both test/ and spec/ exist. Checking with #{chosen.name} only, so changed files under #{skipped.folder} do not run."
170
+ end
171
+
172
+ def steps(sources, tests, base, rails, suite)
173
+ steps = []
174
+ steps << step(TESTS_STEP, { "COVERAGE" => "1" }, test_command(tests, rails, suite)) unless tests.empty?
175
+ return steps if sources.empty?
176
+
177
+ steps << step("Patch coverage", {}, %W[bundle exec simplecov patch --base #{base} --minimum #{PATCH_COVERAGE_MINIMUM}])
178
+ steps << step("Mutation testing", {}, mutation_command(sources, tests, base, rails, suite))
179
+ steps << step("CRAP scores", {}, %W[bundle exec bin/crap --threshold #{CRAP_THRESHOLD} --since #{base}] + sources)
180
+ end
181
+
182
+ def step(title, env, command)
183
+ Step.new(title, sandbox.merge(env), command)
184
+ end
185
+
186
+ def test_command(tests, rails, suite)
187
+ return %w[bundle exec rspec] + tests if suite == RSPEC
188
+ return %w[bin/rails test] + tests if rails
189
+
190
+ %w[bundle exec ruby -Ilib -Itools -Itest -e] + [%(require "test_helper"; ARGV.each { |file| require File.expand_path(file) })] + tests
191
+ end
192
+
193
+ def lint_decision(lint_files)
194
+ return LintDecision.new(nil, "no changed Ruby files to lint") if lint_files.empty?
195
+ return LintDecision.new(nil, "two style guides found: .standard.yml and .rubocop.yml") if ambiguous_style_guide?
196
+
197
+ if rubocop_file?
198
+ return lint_gem_available?(:rubocop) ? LintDecision.new(lint_step(:rubocop, lint_files), nil) : LintDecision.new(nil, "the rubocop gem is not in Gemfile.lock")
199
+ end
200
+
201
+ if standard_file? || dependencies_section.match?(/^ standard(\s|!|$)/)
202
+ return lint_gem_available?(:standard) ? LintDecision.new(lint_step(:standard, lint_files), nil) : LintDecision.new(nil, "the standard gem is not in Gemfile.lock")
203
+ end
204
+
205
+ LintDecision.new(nil, "no style guide found")
206
+ end
207
+
208
+ # Local adaptation (intent 363): RuboCop reads .rubocop_with_todo.yml here, which
209
+ # is .rubocop.yml plus the generated todo. The todo holds this repository's 16,116
210
+ # pre-existing offences, so a lint run reports what the change did and nothing
211
+ # else. .rubocop.yml, the todo-free config, is what the repository is measured
212
+ # against as the todo shrinks.
213
+ def lint_step(guide, lint_files)
214
+ command = guide == :standard ? %w[bundle exec standardrb --force-exclusion] : rubocop_command
215
+ step(LINT_STEP, {}, command + lint_files)
216
+ end
217
+
218
+ def rubocop_command
219
+ base = %w[bundle exec rubocop --force-exclusion]
220
+ exist?(".rubocop_with_todo.yml") ? base + %w[--config .rubocop_with_todo.yml] : base
221
+ end
222
+
223
+ def lint_gem_available?(command_gem)
224
+ return true if command_gem == :rubocop && (dependencies_section.match?(/^ rubocop(\s|!|$)/) ||
225
+ dependencies_section.match?(/^ standard(\s|!|$)/) || dependencies_section.match?(/^ rubocop-rails-omakase(\s|!|$)/))
226
+ return true if command_gem == :standard && dependencies_section.match?(/^ standard(\s|!|$)/)
227
+
228
+ false
229
+ end
230
+
231
+ def ambiguous_style_guide?
232
+ standard_file? && rubocop_file? && !rubocop_config_uses_standard?
233
+ end
234
+
235
+ def rubocop_config_uses_standard?
236
+ config = rubocop_config
237
+ return false if config.empty?
238
+
239
+ config.match?(/inherit_gem:\s*\{?\s*standard:/m) ||
240
+ config.match?(/^(plugins|require):\s*\n(?:[ \t]*-[ \t]*\S+[ \t]*\n)*?[ \t]*-[ \t]*standard[ \t]*$/m) ||
241
+ config.match?(/^(plugins|require):\s*\[[^\]]*\bstandard\b[^\]]*\]/m)
242
+ end
243
+
244
+ def rubocop_config
245
+ exist?(".rubocop.yml") ? File.read(File.join(@root, ".rubocop.yml")) : ""
246
+ end
247
+
248
+ def standard_file?
249
+ exist?(".standard.yml")
250
+ end
251
+
252
+ def rubocop_file?
253
+ exist?(".rubocop.yml")
254
+ end
255
+
256
+ def dependencies_section
257
+ return @dependencies_section if defined?(@dependencies_section)
258
+
259
+ lockfile = exist?("Gemfile.lock") ? File.read(File.join(@root, "Gemfile.lock")) : nil
260
+ unless lockfile
261
+ return @dependencies_section = ""
262
+ end
263
+
264
+ lines = lockfile.lines
265
+ start = lines.index { |line| line.strip == "DEPENDENCIES" }
266
+ unless start
267
+ return @dependencies_section = ""
268
+ end
269
+
270
+ section = []
271
+ lines[(start + 1)..].each do |line|
272
+ break if line.strip.empty?
273
+
274
+ section << line
275
+ end
276
+ @dependencies_section = section.join
277
+ end
278
+
279
+ def mutation_command(sources, tests, base, rails, suite)
280
+ %w[bundle exec mutineer run] + sources + tests.flat_map { |test| ["--test", test] } +
281
+ %W[--since #{base} --threshold #{MUTATION_THRESHOLD}] + (suite == RSPEC ? %w[--framework rspec] : []) +
282
+ (rails ? %w[--rails] : %w[--strategy redefine]) +
283
+ (exist?(".mutineer.yml") ? [] : ["--operators", ALL_OPERATORS.join(",")])
284
+ end
285
+
286
+ def execute(plan, requested_base)
287
+ if plan.sources.empty? && plan.tests.empty? && plan.lint_files.empty?
288
+ @out.puts "No changed Ruby files since #{requested_base}."
289
+ return 0
290
+ end
291
+ unless plan.untested.empty?
292
+ @err.puts "No test file found for: #{plan.untested.join(", ")}. Write one, or pass --test FILE for the tests that cover it."
293
+ return 1
294
+ end
295
+ unless exist?(".mutineer.yml") || plan.sources.empty?
296
+ @err.puts "Missing .mutineer.yml: passing all 11 operators on the command line instead."
297
+ end
298
+
299
+ @out.puts "Base: #{requested_base}"
300
+ @out.puts "Sources: #{plan.sources.empty? ? "none" : plan.sources.join(", ")}"
301
+ @out.puts "Tests: #{plan.tests.join(", ")}"
302
+ @out.puts "Home: #{sandbox["HOME"]}"
303
+ FileUtils.rm_rf(File.join(@root, "coverage"))
304
+ failed = []
305
+ if plan.lint_decision.step
306
+ @out.puts "\n== #{LINT_STEP}"
307
+ failed << plan.lint_decision.step unless @run.call(plan.lint_decision.step.env, plan.lint_decision.step.command)
308
+ elsif plan.lint_decision.skip_reason
309
+ @out.puts "\nLint skipped: #{plan.lint_decision.skip_reason}"
310
+ end
311
+ plan.steps.each do |step|
312
+ @out.puts "\n== #{step.title}"
313
+ next if @run.call(step.env, step.command)
314
+
315
+ failed << step
316
+ next unless step.title == TESTS_STEP
317
+
318
+ @out.puts "\nThe tests are red, so the other checks did not run: a failing test kills every mutant."
319
+ break
320
+ end
321
+ @out.puts failed.empty? ? "\nChange verified" : "\nFailed: #{failed.map(&:title).join(", ")}"
322
+ failed.empty? ? 0 : 1
323
+ end
324
+
325
+ def rails?
326
+ exist?("config/application.rb")
327
+ end
328
+
329
+ def suite
330
+ listed = dependencies_section.match?(RSPEC_DEPENDENCY)
331
+ spec = File.directory?(File.join(@root, "spec")) && (exist?(".rspec") || exist?("spec/spec_helper.rb") || exist?("spec/rails_helper.rb"))
332
+ spec || listed ? RSPEC : MINITEST
333
+ end
334
+
335
+ def exist?(path)
336
+ File.exist?(File.join(@root, path))
337
+ end
338
+
339
+ def usage(code)
340
+ (code.zero? ? @out : @err).puts USAGE
341
+ code
342
+ end
343
+ end
344
+
345
+ exit VerifyChange.new(ARGV).call if $PROGRAM_NAME == __FILE__
package/deprecations.yml CHANGED
@@ -1,6 +1,6 @@
1
1
  # Plastic deprecation notices
2
2
  # Each entry is shown to the user during SessionStart.
3
- # See the plastic-releasing skill for the full convention.
3
+ # See CONTRIBUTING.md's "Cut a release" section for the full convention.
4
4
  #
5
5
  # Schema:
6
6
  # id: unique-slug (used for dismissal tracking)
@@ -2,16 +2,13 @@
2
2
 
3
3
  *Companion to the Operating Manual. How to use Fable as your advisor and planner.*
4
4
 
5
- *Adapted for Plastic (intent 185): this is the shipped reference copy the
6
- `plastic-agent-advisor` skill teaches from. Two named agents carry it,
5
+ *Adapted for Plastic (intent 185): this is the shipped reference copy, reached with
6
+ `plastic help advisor-protocol`. Two named agents carry it,
7
7
  `plastic-primary-advisor` (Primary Advisor) and `plastic-secondary-advisor`
8
8
  (Secondary Advisor, the explicit escalation); the
9
9
  answer shape below shapes the brief and the answer on whichever one you
10
- dispatch, never which file to pick. The
11
- `plastic-agent-advisor` skill reads harness-scoped config
12
- (`advisor.claude.default`, falling back to `plastic-primary-advisor`) to route your
13
- consultation automatically; name a
14
- specific advisor in your request to override it. A frontier model rewards a
10
+ dispatch, never which file to pick. There is no automatic routing between them: the user or
11
+ the main session dispatches one deliberately, by naming it. A frontier model rewards a
15
12
  single, well-formed one-shot brief more than a back-and-forth, so front-load
16
13
  everything section 3 below asks for before you send. The rest of this
17
14
  document is the owner's protocol, unchanged.*
@@ -28,9 +28,9 @@ in 2.0, intent 304; the lead writes the Why and How record itself):
28
28
  reviewed before code; dispatches the executor; applies the risk rule; closes.
29
29
  - **plastic-executor** (Exec): commits the matrix's tests red, writes the code, checks off
30
30
  `checklist.md`, appends `## Insights`, and drives the suite green.
31
- - **the plan reviewer**: a fresh agent on `plastic-intent-executing`'s
32
- `plan-reviewer-prompt.md`, an optional dispatch before any code exists.
33
- - **the post-execution reviewer**: a fresh agent on `code-quality-reviewer-prompt.md`,
31
+ - **the plan reviewer**: a fresh agent on the auto skill's
32
+ `references/plan-reviewer-prompt.md`, an optional dispatch before any code exists.
33
+ - **the post-execution reviewer**: a fresh agent on `references/code-quality-reviewer-prompt.md`,
34
34
  dispatched only when the auto skill's risk rule fires; never the maker.
35
35
 
36
36
  One agent boot (the executor) is the minimum delivery; the plan reviewer is a second,
@@ -118,14 +118,14 @@ written path, and the lead verifies state from the files (`plastic-lock status`,
118
118
  ### Delegation
119
119
 
120
120
  The roles are thin handoff contracts, not a spawning engine. Dispatch runs through Plastic's
121
- own engine, `plastic-intent-executing`: one executor for the consolidated action, the two
121
+ own engine, `plastic intent step`: one executor for the consolidated action, the two
122
122
  reviewer prompts as fresh agents. The team model defines who hands what to whom and where the
123
123
  reviews sit; the engine does the actual spawning.
124
124
 
125
125
  ### Fallback by Case
126
126
 
127
127
  If the harness supports agent dispatch, auto mode dispatches through
128
- `plastic-intent-executing`. If the harness has no agent dispatch at all (Codex CLI today), the
128
+ `plastic intent step`. If the harness has no agent dispatch at all (Codex CLI today), the
129
129
  lead walks the five steps itself: it still writes the matrix and the tests first, and reviews
130
130
  its own plan against the matrix before code, saying so in `## Insights`.
131
131
 
@@ -147,8 +147,8 @@ tests first, one suite run per intent.
147
147
  ## Autonomous Delivery
148
148
 
149
149
  Human owns What and Why for human-initiated intents. The team assists (research, exploration)
150
- but the human drives until handoff. When Why is complete, or the human triggers `plastic-auto`,
151
- the auto team takes over How and Exec autonomously.
150
+ but the human drives until handoff. When Why is complete, or the human runs
151
+ `plastic auto take ID`, the auto team takes over How and Exec autonomously.
152
152
 
153
153
  - **Safe-by-default:** the executor always prefers non-destructive routes (rename vs delete,
154
154
  additive migrations, backups before changes). Destructive actions on existing projects
@@ -39,4 +39,4 @@ INDEX but the lock is still present or stale). Finishing the tail is FINISHING a
39
39
  a done intent is never moved back to `## Active`.
40
40
 
41
41
  One report per audience: a delivery produces `outcome.md` plus one EM-to-CTO owner report, and
42
- no other step restates either (see `skills/auto/references/human-report-contract.md`).
42
+ no other step restates either (see `plastic help human-report-contract`).
@@ -29,27 +29,25 @@ written by eye:
29
29
 
30
30
  ## Binding table (intent 331f)
31
31
 
32
- Every skill that shows state names its own report verb, one row per skill and trigger. Each
33
- bound skill's file carries the SAME rule next to its verb: print the screen as the first
32
+ Every command or lead role that shows state names its own report verb, one row per binding
33
+ and trigger. Each one carries the SAME rule next to its verb: print the screen as the first
34
34
  characters of the reply, nothing before it, no fence, or the hook cannot paint it.
35
35
 
36
- | Skill | Trigger | Verb |
36
+ | Command or lead | Trigger | Verb |
37
37
  |---|---|---|
38
- | `plastic-intent-continuing` | project route (continue, load project) | `dashboard.rb ... --screen` |
39
- | `plastic-intent-continuing` | a named intent | `report-screen state` |
40
- | `plastic-intent-continuing` | "where are we" (a status ask) | `report-screen session` |
41
- | `plastic-intent-continuing` | "why so long" | `report-screen delay` |
42
- | `plastic-intent-continuing` | a roadmap route | `report-screen roadmap ... state` |
43
- | `plastic-auto` | the How boundary, before the executor | `report-screen plan` |
44
- | `plastic-auto` | each of the five triggers | `report-screen state` |
45
- | `plastic-auto` | close | `report-screen delivered` |
46
- | `plastic-intent-ending` | the close | `report-screen delivered` |
47
- | `plastic-intent-speccing` | the action files are written | `report-screen plan` |
48
- | `plastic-roadmap` | create | `report-screen roadmap ... plan` |
49
- | `plastic-roadmap` | read | `report-screen roadmap ... state` |
50
- | `plastic-roadmap` | close | `report-screen roadmap ... delivered` |
51
- | `plastic-dashboard` | any invocation | `dashboard.rb ... --screen` |
52
- | `plastic-intent-executing` | after the red commit, and after the suite | `report-screen state` |
38
+ | `plastic continue` | project route (continue, load project) | `dashboard.rb ... --screen` |
39
+ | `plastic continue` | a named intent | `report-screen state` |
40
+ | `plastic continue` | "where are we" (a status ask) | `report-screen session` |
41
+ | `plastic continue` | "why so long" | `report-screen delay` |
42
+ | `plastic continue` | a roadmap route | `report-screen roadmap ... state` |
43
+ | the auto team's lead | the How boundary, before the executor | `report-screen plan` |
44
+ | the auto team's lead | each of the five triggers | `report-screen state` |
45
+ | the auto team's lead | close | `report-screen delivered` |
46
+ | `plastic intent end` | the close | `report-screen delivered` |
47
+ | `plastic intent spec` | the action files are written | `report-screen plan` |
48
+ | `plastic roadmap show` | any invocation | `report-screen roadmap ... state` |
49
+ | `plastic status` | any invocation | in-process (`Scope#stores`) |
50
+ | `plastic intent step` | after the red commit, and after the suite | `report-screen state` |
53
51
 
54
52
  ## A roadmap's own three reports (intent 331c)
55
53
 
@@ -104,7 +102,7 @@ A delivery still ends with `outcome.md` plus one `delivered` screen.
104
102
 
105
103
  A delivery produces exactly two artifacts: `outcome.md` (generated by `scripts/end-intent`
106
104
  from `graph.md`, `nodes/`, and the ledger when the intent has one, intent 339; hand-authored
107
- by `plastic-intent-ending` otherwise) and one `delivered` screen at the End stage. No stage or skill restates a delivery already
105
+ by `plastic intent end` otherwise) and one `delivered` screen at the End stage. No stage or skill restates a delivery already
108
106
  written to `outcome.md`; point at it instead. Skills do not open with a banner that names the
109
107
  skill or restates the intent id and name the owner just typed. Announce only what the reader
110
108
  cannot already know: an error, a result, a choice with its reason, or a handoff.
@@ -6,8 +6,8 @@ This chapter holds the full roadmap file format and its relationship to INDEX.md
6
6
 
7
7
  Roadmaps exist for planned parallel delivery of intents in a coherent and organized way. A roadmap
8
8
  is a named, ordered, delivery-side collection of intents: the delivery-side counterpart to a
9
- release (completion-side, tracked in `CHANGELOG.md`). Use `plastic-roadmap` to create, order,
10
- close, and consume one.
9
+ release (completion-side, tracked in `CHANGELOG.md`). Create one by hand from the template, then
10
+ use `plastic roadmap show`, `next`, `log`, and `check` to read, drive, and audit it.
11
11
 
12
12
  File location: `roadmaps/{slug}.md`, a sibling of `INDEX.md`, wherever `INDEX.md` lives, never
13
13
  inside `store/` (store holds intent directories, not project artifacts). For a project that is its