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

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 +345 -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 +65 -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 +4 -6
  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
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Plastic::CLI::TABLE (intent 363) - the whole command line in one frozen hash:
4
+ # a command name mapped to the file that holds it, the class inside
5
+ # Plastic::CLI::Commands, and the one line `plastic help` prints beside it.
6
+ #
7
+ # Adding a command is one row here plus one file. `plastic help` reads this
8
+ # hash and loads nothing, so listing the commands costs no command file.
9
+ # test/cli/table_test.rb walks every row, so a half-wired command cannot ship.
10
+ module Plastic
11
+ class CLI
12
+ TABLE = {
13
+ "auto brief" => ["commands/auto_brief", "AutoBrief", "Print an intent's spawn preamble, and the advisor shapes for --role advisor"],
14
+ "auto lock" => ["commands/auto_lock", "AutoLock", "Inspect, repair, or release an intent's delivery lock"],
15
+ "auto report" => ["commands/auto_report", "AutoReport", "Print an intent's completion report, then the review-by-risk rules"],
16
+ "auto take" => ["commands/auto_take", "AutoTake", "Arm an intent's delivery lock for this session"],
17
+ "auto" => ["commands/auto", "Auto", "List the auto subcommands"],
18
+ "backup" => ["commands/backup", "Backup", "Write one archive of the three databases and the config"],
19
+ "checkout" => ["commands/checkout", "Checkout", "Restore missing store files from the databases"],
20
+ "continue" => ["commands/continue", "Continue", "Show where one project stands and what runs next"],
21
+ "doctor" => ["commands/doctor", "Doctor", "Diagnose Plastic installation health"],
22
+ "feedback" => ["commands/feedback", "Feedback", "File a feedback report, body on standard input"],
23
+ "help" => ["commands/help", "Help", "List the commands and help topics, or show one usage"],
24
+ "hook" => ["commands/hook", "Hook", "Run one harness hook event through its launcher"],
25
+ "index" => ["commands/index", "Index", "Rebuild the search index from every markdown file in the stores"],
26
+ "install" => ["commands/install", "Install", "Install Plastic into this machine's agents"],
27
+ "intent" => ["commands/intent", "Intent", "List the intent subcommands"],
28
+ "intent answer" => ["commands/intent_answer", "IntentAnswer", "Answer an intent's needs_decision node"],
29
+ "intent end" => ["commands/intent_end", "IntentEnd", "Close an intent as delivered or abandoned"],
30
+ "intent new" => ["commands/intent_new", "IntentNew", "Create a new intent"],
31
+ "intent note" => ["commands/intent_note", "IntentNote", "Append a savepoint note to an intent"],
32
+ "intent rule" => ["commands/intent_rule", "IntentRule", "Record a ruling in an intent's Insights"],
33
+ "intent show" => ["commands/intent_show", "IntentShow", "Print an intent's state screen"],
34
+ "intent spec" => ["commands/intent_spec", "IntentSpec", "Print an intent's state screen, then the speccing rules"],
35
+ "intent step" => ["commands/intent_step", "IntentStep", "Run the next ready step of an intent's graph"],
36
+ "intent verify" => ["commands/intent_verify", "IntentVerify", "Run an intent's merge-gate checks"],
37
+ "migrate" => ["commands/migrate", "Migrate", "List the migrate subcommands"],
38
+ "migrate stores" => ["commands/migrate_stores", "MigrateStores", "Move the stores under stores/, behind a full copy of the home"],
39
+ "next" => ["commands/next", "Next", "Print the next action in one line"],
40
+ "project" => ["commands/project", "Project", "List the project subcommands"],
41
+ "project links" => ["commands/project_links", "ProjectLinks", "Project every store's Links sections from frontmatter"],
42
+ "project list" => ["commands/project_list", "ProjectList", "List every store this machine holds"],
43
+ "project new" => ["commands/project_new", "ProjectNew", "Register, provision and validate a new project"],
44
+ "query" => ["commands/query", "Query", "Run one read-only SQL statement on the index"],
45
+ "render" => ["commands/render", "Render", "Print one markdown file as an HTML page"],
46
+ "roadmap" => ["commands/roadmap", "Roadmap", "List the roadmap subcommands"],
47
+ "roadmap check" => ["commands/roadmap_check", "RoadmapCheck", "Check a roadmap's graph for cycles and dangling ids"],
48
+ "roadmap log" => ["commands/roadmap_log", "RoadmapLog", "Append a savepoint line to a roadmap's ledger"],
49
+ "roadmap next" => ["commands/roadmap_next", "RoadmapNext", "Name the roadmap most worth continuing"],
50
+ "roadmap show" => ["commands/roadmap_show", "RoadmapShow", "Print a roadmap's state screen"],
51
+ "rollback" => ["commands/rollback", "Rollback", "Move to a Plastic version this machine has run before"],
52
+ "search" => ["commands/search", "Search", "Find text in the stores, ranked, with one excerpt each"],
53
+ "session commit" => ["commands/session_commit", "SessionCommit", "Commit one verified checklist item"],
54
+ "session handoff" => ["commands/session_handoff", "SessionHandoff", "Write this session's hand-off into the day ledger"],
55
+ "session summary" => ["commands/session_summary", "SessionSummary", "Print the day ledger's open items and recent activity"],
56
+ "session" => ["commands/session", "Session", "List the session subcommands"],
57
+ "status" => ["commands/status", "Status", "Show active work in every store"],
58
+ "sync" => ["commands/sync", "Sync", "Bring the store files and the three databases level"],
59
+ "uninstall" => ["commands/uninstall", "Uninstall", "Remove Plastic from this machine's agents"],
60
+ "update" => ["commands/update", "Update", "Move Plastic to the next version on its channel"],
61
+ "version" => ["commands/version", "Version", "Print the installed Plastic version"]
62
+ }.freeze
63
+ end
64
+ end
@@ -0,0 +1,94 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
4
+ require "did_you_mean"
5
+ require_relative "cli/table"
6
+ require_relative "cli/command"
7
+ require_relative "cli/output"
8
+
9
+ # Plastic::CLI (intent 363) - the dispatcher behind bin/plastic. It reads the
10
+ # command table, matches the longest command name the arguments begin with,
11
+ # requires that one file, and calls it. Nothing else is loaded, which is what
12
+ # keeps a call at about 20 milliseconds with RubyGems off.
13
+ #
14
+ # No arguments, or --help, print the command list. --version prints the
15
+ # version. A command's own --help prints that command's usage line, read from
16
+ # the class without building it. An unknown name exits 2 and names the closest
17
+ # command, so a typo never becomes a stack trace.
18
+ module Plastic
19
+ class CLI
20
+ FLAGS = {"--version" => "version", "-v" => "version",
21
+ "--help" => "help", "-h" => "help"}.freeze
22
+
23
+ def self.call(...)
24
+ new(...).call
25
+ end
26
+
27
+ def initialize(argv, out: $stdout, err: $stderr, env: ENV, home: Dir.home,
28
+ directory: Dir.pwd, table: TABLE)
29
+ @argv = argv.dup
30
+ @out = out
31
+ @err = err
32
+ @env = env
33
+ @home = home
34
+ @directory = directory
35
+ @table = table
36
+ end
37
+
38
+ def call
39
+ matched = match(expanded)
40
+ return unknown(expanded.first) unless matched
41
+
42
+ name, rest = matched
43
+ return usage(name, rest) if help?(rest)
44
+
45
+ command(name).call(rest, out: @out, err: @err, env: @env, home: @home,
46
+ directory: @directory)
47
+ end
48
+
49
+ def match(argv)
50
+ [2, 1].each do |width|
51
+ name = argv.first(width).join(" ")
52
+ return [name, argv.drop(width)] if @table.key?(name)
53
+ end
54
+ nil
55
+ end
56
+
57
+ private
58
+
59
+ def expanded
60
+ return ["help"] if @argv.empty?
61
+
62
+ [FLAGS.fetch(@argv.first, @argv.first)] + @argv.drop(1)
63
+ end
64
+
65
+ def help?(rest)
66
+ rest.any? { |argument| FLAGS[argument] == "help" }
67
+ end
68
+
69
+ def usage(name, rest)
70
+ Output.new(out: @out, err: @err)
71
+ .row("usage", command(name)::USAGE_LINE)
72
+ .flush(json: rest.include?("--json"))
73
+ Command::OK
74
+ end
75
+
76
+ def command(name)
77
+ file, const, = @table.fetch(name)
78
+ require_relative "cli/#{file}"
79
+ Commands.const_get(const)
80
+ end
81
+
82
+ def unknown(name)
83
+ suggestions = closest(name)
84
+ @err.puts "plastic: no command named #{name.inspect}"
85
+ @err.puts "Closest: #{suggestions.join(", ")}" unless suggestions.empty?
86
+ @err.puts "Run `plastic help` for the command list."
87
+ Command::USAGE
88
+ end
89
+
90
+ def closest(name)
91
+ DidYouMean::SpellChecker.new(dictionary: @table.keys).correct(name)
92
+ end
93
+ end
94
+ end
@@ -28,7 +28,15 @@ module CompactInstructions
28
28
  # as stale, so the block states the shipped numbers and names the two keys as the
29
29
  # override instead. It names the hand-off in words and never by path, so it reads
30
30
  # correctly whether or not the hand-off writer is installed.
31
+ # The `@` line is a Claude Code import: the harness reads the named file into the
32
+ # session. It is the whole of ruling D43, that PLASTIC.md is the only instruction text
33
+ # Plastic carries. It names the installed copy under the Plastic home, because a bare
34
+ # `@PLASTIC.md` would resolve against ~/.claude, where no such file exists.
31
35
  BODY = <<~MD.freeze
36
+ Plastic runs from one command, `plastic`. Its instructions are one page, imported here:
37
+
38
+ @~/.plastic/PLASTIC.md
39
+
32
40
  Plastic watches this session's context. When the harness reports how much of the
33
41
  window is used:
34
42
 
@@ -8,6 +8,7 @@
8
8
  # heartbeat. Never the raw ledger (296 D36). No environment reads; every
9
9
  # path is injected.
10
10
 
11
+ require_relative "store_layout"
11
12
  require "time"
12
13
  require_relative "session_ledger"
13
14
  require_relative "handoff"
@@ -82,10 +83,10 @@ module DaySummary
82
83
  # The global store plus every projects/<slug>/store, each with its INDEX
83
84
  # one level up, the way doctor enumerates them.
84
85
  def stores(home)
85
- list = [[File.join(home, "INDEX.md"), File.join(home, "store")]]
86
- projects_root = File.join(home, "projects")
86
+ list = [[File.join(Plastic::StoreLayout.global_root(home), "INDEX.md"), Plastic::StoreLayout.global_store(home)]]
87
+ projects_root = Plastic::StoreLayout.projects_root(home)
87
88
  if File.directory?(projects_root)
88
- Dir.children(projects_root).sort.each do |slug|
89
+ Plastic::StoreLayout.project_slugs(home).each do |slug|
89
90
  list << [File.join(projects_root, slug, "INDEX.md"), File.join(projects_root, slug, "store")]
90
91
  end
91
92
  end
@@ -8,12 +8,13 @@
8
8
  # the store, conventions, intent and CLI halves, so Doctor stays one class with
9
9
  # one public surface and one definition of which checks are core (intent 36a).
10
10
 
11
+ require_relative "store_layout"
11
12
  require "json"
12
13
  require "yaml"
13
14
  require "time"
14
15
  require "digest"
15
- require "rubygems"
16
16
 
17
+ require_relative "version_number"
17
18
  require_relative "hook_registry"
18
19
  require_relative "compact_instructions"
19
20
 
@@ -301,7 +302,7 @@ class Doctor
301
302
  return checks
302
303
  end
303
304
 
304
- index_path = File.join(plastic_home, "INDEX.md")
305
+ index_path = File.join(Plastic::StoreLayout.global_root(plastic_home), "INDEX.md")
305
306
  if File.exist?(index_path)
306
307
  checks << check(
307
308
  category: "global_store", name: "global_index_reachable", status: "pass",
@@ -517,9 +518,6 @@ class Doctor
517
518
  checks << hooks_entries_owned_check(settings)
518
519
  end
519
520
 
520
- # skills_exist — flat, hyphen-namespaced personal skills (plastic-<name>/)
521
- checks << flat_skills_check(agent_dir, "--claude")
522
-
523
521
  # stray_skills — installed plastic-* skill dir with no manifest entry (a leftover,
524
522
  # e.g. an old-name copy after a rename; intent 158a AC15)
525
523
  checks << stray_skills_check(agent_dir, "--claude", File.join(agent_dir, "plastic", "manifest.json"))
@@ -630,24 +628,6 @@ class Doctor
630
628
  )
631
629
  end
632
630
 
633
- def flat_skills_check(agent_dir, installer_flag)
634
- skills_root = File.join(agent_dir, "skills")
635
- found = Dir.glob(File.join(skills_root, "plastic-*", "SKILL.md"))
636
-
637
- if !found.empty?
638
- check(
639
- category: "agent_registration", name: "skills_exist", status: "pass",
640
- message: "#{found.size} plastic-* skill(s) installed in #{tilde(skills_root)}"
641
- )
642
- else
643
- check(
644
- category: "agent_registration", name: "skills_exist", status: "fail",
645
- message: "No plastic-* skills found in #{tilde(skills_root)}",
646
- fixable: true, fix_hint: "Re-run the Plastic installer: npx @zalom/plastic@latest #{installer_flag}"
647
- )
648
- end
649
- end
650
-
651
631
  # Manifest-diff stray-skill check (intent 158a), extended (intent 276) to
652
632
  # never vanish on a missing manifest and to name the reserved-prefix rule.
653
633
  def stray_skills_check(agent_dir, installer_flag, manifest_path)
@@ -707,9 +687,6 @@ class Doctor
707
687
  def check_flat_skills_and_stray(agent_key, agent_dir)
708
688
  checks = []
709
689
 
710
- # For codex/hermes: just check skills exist (no settings.json hooks)
711
- checks << flat_skills_check(agent_dir, "--#{agent_key}")
712
-
713
690
  # stray_skills — installed plastic-* skill dir with no manifest entry (a leftover,
714
691
  # e.g. an old-name copy after a rename; intent 158a AC15)
715
692
  checks << stray_skills_check(agent_dir, "--#{agent_key}", File.join(agent_dir, "plastic", "manifest.json"))
@@ -1061,9 +1038,7 @@ class Doctor
1061
1038
  end
1062
1039
 
1063
1040
  def safe_version(str)
1064
- Gem::Version.new(str.to_s)
1065
- rescue ArgumentError
1066
- nil
1041
+ VersionNumber.parse(str)
1067
1042
  end
1068
1043
 
1069
1044
  def check_core_files(agent_key, include_drift: true)
@@ -1167,7 +1142,7 @@ class Doctor
1167
1142
  "#{tilde(agent_version_path)}: #{agent_version}",
1168
1143
  ],
1169
1144
  fixable: true,
1170
- fix_hint: "Re-sync the stale harness: npx @zalom/plastic@latest install --reinstall <flag>, or plastic-rollback to a prior version"
1145
+ fix_hint: "Re-sync the stale harness: npx @zalom/plastic@latest install --reinstall <flag>, or `plastic rollback` to a prior version"
1171
1146
  )
1172
1147
  end
1173
1148
  else
@@ -1175,7 +1150,7 @@ class Doctor
1175
1150
  category: "core_files", name: "version_match", status: "warn",
1176
1151
  message: "Agent-side VERSION file not found at #{tilde(agent_version_path)}",
1177
1152
  fixable: true,
1178
- fix_hint: "Re-sync the stale harness: npx @zalom/plastic@latest install --reinstall <flag>, or plastic-rollback to a prior version"
1153
+ fix_hint: "Re-sync the stale harness: npx @zalom/plastic@latest install --reinstall <flag>, or `plastic rollback` to a prior version"
1179
1154
  )
1180
1155
  end
1181
1156
  end
@@ -1280,7 +1255,7 @@ class Doctor
1280
1255
 
1281
1256
  DISPLAY_HOOK_FIX_HINT = "Re-run the Plastic installer to repair the hook registration: " \
1282
1257
  "npx @zalom/plastic@<channel> install --reinstall --claude " \
1283
- "(plastic-install --repair)".freeze
1258
+ "(plastic install --reinstall)".freeze
1284
1259
 
1285
1260
  # display_hook_registered (intent 331e, D1, category "display"): the Claude
1286
1261
  # settings carry the plastic-message-display command, on-disk, executable.
@@ -1,6 +1,7 @@
1
1
  # encoding: UTF-8
2
2
  # frozen_string_literal: true
3
3
 
4
+ require_relative "store_layout"
4
5
  require "time"
5
6
  require_relative "session_ledger"
6
7
 
@@ -38,7 +39,7 @@ module DoctorSessionLedger
38
39
  def check_session_ledger(scopes: nil, now: Time.now)
39
40
  return [] unless scopes.nil? || scopes.include?("global")
40
41
 
41
- store_dir = File.join(plastic_home, "store")
42
+ store_dir = Plastic::StoreLayout.global_store(plastic_home)
42
43
  orphans = orphaned_session_dirs(store_dir, now)
43
44
  shape = day_ledger_shape_problems(store_dir)
44
45
 
@@ -18,7 +18,7 @@ require "fileutils"
18
18
  # Trust model: this class never sends anything anywhere. `compose` returns a
19
19
  # Result carrying a local file path and a browser URL; only the human, in
20
20
  # their own authenticated browser, submits it. There is no send method here
21
- # and there must never be one (see skills/feedback/references/transport-and-privacy.md).
21
+ # and there must never be one (see SECURITY.md).
22
22
  class FeedbackReport
23
23
  GITHUB_REPO = "zalom/plastic"
24
24
  CAP_BYTES = 7500
@@ -1,6 +1,7 @@
1
1
  # encoding: UTF-8
2
2
  # frozen_string_literal: true
3
3
 
4
+ require_relative "store_layout"
4
5
  require "json"
5
6
  require "tmpdir"
6
7
  require_relative "node_ledger"
@@ -112,7 +113,8 @@ module GraphMeasureModels
112
113
  # purpose - two different silences over the same store, agreeing with
113
114
  # each other only by accident.
114
115
  def project_config_path(store_dir)
115
- File.join(File.dirname(store_dir.to_s), "config.yml")
116
+ home, slug = Plastic::StoreLayout.locate(store_dir.to_s)
117
+ File.join((slug == Plastic::StoreLayout::GLOBAL) ? home : File.dirname(store_dir.to_s), "config.yml")
116
118
  end
117
119
 
118
120
  # --- rendering ---------------------------------------------------------------
@@ -10,6 +10,8 @@
10
10
  # plain hyphen as the id/title separator on READ; every WRITE still emits the
11
11
  # real em dash (D10). The separator is built from the codepoint, not a
12
12
  # literal byte, so this file stays em-dash free.
13
+ require_relative "atomic_write"
14
+
13
15
  module IndexEntry
14
16
  module_function
15
17
 
@@ -22,6 +24,13 @@ module IndexEntry
22
24
  line.to_s.match(ENTRY_RE)
23
25
  end
24
26
 
27
+ def add_active(index_path, dir_name:, title:)
28
+ id = dir_name.split("--").first
29
+ line = "- [#{id} #{EM_DASH} #{title}](store/#{dir_name}/#{dir_name}.md)\n"
30
+ text = File.read(index_path, encoding: "UTF-8")
31
+ AtomicWrite.write(index_path, text.sub(/^## Active\n/) { |heading| heading + line })
32
+ end
33
+
25
34
  # True iff the intent is Active in its store's INDEX.md, which lives at the
26
35
  # PARENT of the store/ dir. Non-raising: any failure (missing or unreadable
27
36
  # INDEX, bad arg) returns false. `index_active_ids` is a pure-data test
@@ -1,6 +1,7 @@
1
1
  # encoding: UTF-8
2
2
  # frozen_string_literal: true
3
3
 
4
+ require_relative "store_layout"
4
5
  require "json"
5
6
  require "yaml"
6
7
  require "fileutils"
@@ -12,6 +13,7 @@ require_relative "agent_models"
12
13
  require_relative "harness_text"
13
14
  require_relative "compact_instructions"
14
15
  require_relative "engine_permissions"
16
+ require_relative "qmd_sync"
15
17
 
16
18
  # Shared installer machinery, instantiable with injected package root / store / agent
17
19
  # map so the verb scripts (install/update/uninstall/rollback) and their tests can run
@@ -45,23 +47,21 @@ class InstallerCore
45
47
  CLAUDE_SECTION_END = "<!-- END PLASTIC COMPACT -->"
46
48
  CLAUDE_SECTION_RE = /^<!-- BEGIN PLASTIC COMPACT.*?-->\n.*?\n<!-- END PLASTIC COMPACT -->\n?/m
47
49
 
48
- # Curated essentials plus a pointer to ~/.plastic/PLASTIC.md and the plastic-conventions
49
- # skill, injected into ~/.codex/AGENTS.md. Not a slice of PLASTIC.md itself: AGENTS.md is
50
- # a shared file Codex merges from multiple sources, so this block stays a small,
51
- # hand-curated pointer rather than embedding the core wholesale, and it never drifts
52
- # because it only ever points, never duplicates.
50
+ # Curated essentials plus a pointer to ~/.plastic/PLASTIC.md and the help topics
51
+ # `plastic help TOPIC` prints, injected into ~/.codex/AGENTS.md. Not a slice of
52
+ # PLASTIC.md itself: AGENTS.md is a shared file Codex merges from multiple sources,
53
+ # so this block stays a small, hand-curated pointer rather than embedding the core
54
+ # wholesale, and it never drifts because it only ever points, never duplicates.
53
55
  CODEX_AGENTS_MD_BODY = <<~MD.freeze
54
56
  Plastic is installed for this agent. Plastic is intent-driven state management: work runs
55
57
  in one of three modes, direct, thinking, or auto (a team drives the runner loop:
56
58
  `runner step`, `status`, `answer`). Do not jump straight to code.
57
59
 
58
60
  Standing rules:
59
- - Core conventions live in ~/.plastic/PLASTIC.md. Read it and follow it exactly. For
60
- depth, read a chapter from ~/.agents/skills/plastic-conventions/references/ on demand.
61
- Both are generated and overwritten on Plastic updates, so never edit them.
62
- - Operational procedures are installed as skills under ~/.agents/skills/ (each
63
- plastic-<name>/SKILL.md). Invoke one explicitly as $plastic-<name> (for example
64
- $plastic-doctor), or let Codex pick one implicitly by matching its description.
61
+ - The command line is ~/.plastic/PLASTIC.md. Read it and follow it exactly. The
62
+ conventions are chapters `plastic help TOPIC` prints on demand. Both are
63
+ generated and overwritten on Plastic updates, so never edit them.
64
+ - Operational procedures are the `plastic` command line itself, run directly.
65
65
  - Intents, specs, plans, checklists, and outcomes live under ~/.plastic/, never in
66
66
  the project tree.
67
67
 
@@ -352,12 +352,36 @@ class InstallerCore
352
352
  end
353
353
  end
354
354
 
355
+ # The command line (intent 363). Glob-derived for the reason screen_files is:
356
+ # a command is one row in scripts/lib/cli/table.rb plus one file, and a
357
+ # hand-written manifest would make it two files and a diff. The launcher
358
+ # bin/plastic requires scripts/lib/cli.rb by a relative path, so the whole
359
+ # subtree travels together or an installed copy LoadErrors on the first call.
360
+ def cli_files
361
+ Dir.glob(File.join(package_root, "scripts", "lib", "{cli,rlm}", "**", "*.rb")).each_with_object({}) do |path, acc|
362
+ rel = path.sub("#{package_root}/", "")
363
+ acc[rel] = rel
364
+ end
365
+ end
366
+
367
+ # `plastic help TOPIC` (intent 372, family 4) reads docs/help/TOPIC.md from
368
+ # package_root the same way it reads a command's file, so the chapter has to
369
+ # ship the same way a template does: glob-derived, one file added and it
370
+ # installs with no diff here.
371
+ def help_files
372
+ Dir.glob(File.join(package_root, "docs", "help", "*.md")).each_with_object({}) do |path, acc|
373
+ rel = File.join("docs", "help", File.basename(path))
374
+ acc[rel] = rel
375
+ end
376
+ end
377
+
355
378
  # Files copied into ~/.plastic on install/update. Every verb script + the shared lib
356
379
  # must be here so the installed ~/.plastic/scripts copy is self-complete (sync-guarded
357
380
  # by install_sync_test). The templates and screen-kind halves are glob-derived
358
381
  # (template_files, screen_files above); the rest stays a hand-written literal.
359
382
  def core_files
360
- hand_registered_files.merge(template_files).merge(hook_files).merge(screen_files)
383
+ hand_registered_files.merge(template_files).merge(hook_files).merge(screen_files).merge(cli_files)
384
+ .merge(help_files)
361
385
  end
362
386
 
363
387
  def hand_registered_files
@@ -391,6 +415,9 @@ class InstallerCore
391
415
  "scripts/plastic-lock" => "scripts/plastic-lock",
392
416
  "scripts/lib/hook_registry.rb" => "scripts/lib/hook_registry.rb",
393
417
  "scripts/lib/compact_instructions.rb" => "scripts/lib/compact_instructions.rb",
418
+ "scripts/lib/version_number.rb" => "scripts/lib/version_number.rb",
419
+ "scripts/lib/cli.rb" => "scripts/lib/cli.rb",
420
+ "bin/plastic" => "bin/plastic",
394
421
  "scripts/agent-report" => "scripts/agent-report",
395
422
  "scripts/lib/insights.rb" => "scripts/lib/insights.rb",
396
423
  "scripts/insight-append" => "scripts/insight-append",
@@ -406,6 +433,14 @@ class InstallerCore
406
433
  "scripts/lib/intent_validator.rb" => "scripts/lib/intent_validator.rb",
407
434
  "scripts/lib/graph_rebuild.rb" => "scripts/lib/graph_rebuild.rb",
408
435
  "scripts/lib/store_discovery.rb" => "scripts/lib/store_discovery.rb",
436
+ "scripts/lib/store_layout.rb" => "scripts/lib/store_layout.rb",
437
+ "scripts/lib/stores_move.rb" => "scripts/lib/stores_move.rb",
438
+ "scripts/lib/sqlite.rb" => "scripts/lib/sqlite.rb",
439
+ "scripts/lib/search_index.rb" => "scripts/lib/search_index.rb",
440
+ "scripts/lib/store_sync.rb" => "scripts/lib/store_sync.rb",
441
+ "scripts/lib/work_graph.rb" => "scripts/lib/work_graph.rb",
442
+ "scripts/lib/reference_archive.rb" => "scripts/lib/reference_archive.rb",
443
+ "scripts/lib/backup.rb" => "scripts/lib/backup.rb",
409
444
  "scripts/lib/frontmatter_writer.rb" => "scripts/lib/frontmatter_writer.rb",
410
445
  "scripts/lib/links_projection.rb" => "scripts/lib/links_projection.rb",
411
446
  "scripts/lib/links_section.rb" => "scripts/lib/links_section.rb",
@@ -624,7 +659,7 @@ class InstallerCore
624
659
  def bootstrap
625
660
  puts " \u{1f331} First install \u{2014} bootstrapping store..."
626
661
 
627
- FileUtils.mkdir_p(File.join(plastic_home, "store"))
662
+ FileUtils.mkdir_p(Plastic::StoreLayout.global_store(plastic_home))
628
663
  FileUtils.mkdir_p(File.join(plastic_home, "projects"))
629
664
 
630
665
  write_if_missing(File.join(plastic_home, "config.yml"), <<~YAML)
@@ -643,7 +678,7 @@ class InstallerCore
643
678
 
644
679
  write_if_missing(File.join(plastic_home, "projects.yml"), "---\nprojects: {}\n")
645
680
 
646
- write_if_missing(File.join(plastic_home, "INDEX.md"), <<~MD)
681
+ write_if_missing(File.join(Plastic::StoreLayout.global_root(plastic_home), "INDEX.md"), <<~MD)
647
682
  # Index
648
683
 
649
684
  ## Active
@@ -661,7 +696,7 @@ class InstallerCore
661
696
  # Plastic: Agent Instructions
662
697
 
663
698
  Read `PLASTIC.md` in this directory for the core conventions; deeper doctrine lives
664
- in the `plastic-conventions` skill's chapters. Follow it exactly. Never modify it:
699
+ in the chapters `plastic help TOPIC` prints. Follow it exactly. Never modify it:
665
700
  it is overwritten on plugin updates.
666
701
 
667
702
  This file (`AGENTS.md`) is where project-specific rules live.
@@ -672,6 +707,25 @@ class InstallerCore
672
707
  puts " \u{2705} Store bootstrapped"
673
708
  end
674
709
 
710
+ # Intent 372 (former install skill, lines 119-127): `~/.plastic` becomes its own
711
+ # git repository the first time it exists, so `update` can later commit the core
712
+ # files it re-syncs. Leaves an existing repository alone.
713
+ def git_init_if_absent(runner: ->(cmd) { system(*cmd) })
714
+ FileUtils.mkdir_p(plastic_home)
715
+ return if File.directory?(File.join(plastic_home, ".git"))
716
+ runner.call(["git", "-C", plastic_home, "init", "-q"])
717
+ end
718
+
719
+ # Intent 372 (former install skill, lines 167-179): register every Plastic store as
720
+ # a QMD collection. QmdSync.register already no-ops per store when QMD is absent;
721
+ # the detector is injected too so a run stays hermetic on a machine that happens to
722
+ # have the real `qmd` binary on PATH.
723
+ def register_with_qmd(runner: QmdSync.default_runner, detector: QmdSync.method(:detect))
724
+ QmdSync.enumerate_stores(plastic_home: plastic_home).each do |store|
725
+ QmdSync.register(collection: store[:collection], dir: store[:dir], runner: runner, detector: detector)
726
+ end
727
+ end
728
+
675
729
  # --- Agent adapters ---
676
730
 
677
731
  # Uniform per-agent install record dir: <agent-dir>/plastic/ holds VERSION and
@@ -943,9 +997,9 @@ class InstallerCore
943
997
  # Every store root (the global store, plus each project store) whose real
944
998
  # path is reached with no symlink between it and plastic_home.
945
999
  def real_store_roots(plastic_home)
946
- roots = [File.join(plastic_home, "store")] +
947
- Dir.glob(File.join(plastic_home, "projects", "*", "store"))
948
- roots.select { |root| real_store_root?(plastic_home, root) }
1000
+ roots = [Plastic::StoreLayout.global_store(plastic_home)] +
1001
+ Dir.glob(File.join(Plastic::StoreLayout.projects_root(plastic_home), "*", "store"))
1002
+ roots.uniq.select { |root| real_store_root?(plastic_home, root) }
949
1003
  end
950
1004
 
951
1005
  def real_store_root?(plastic_home, store_root)
@@ -1020,7 +1074,7 @@ class InstallerCore
1020
1074
  # fails milder than the bug it prevents, independent of what any caller's `root:` is or
1021
1075
  # what a hand-edited manifest claims.
1022
1076
  def store_roots
1023
- [File.join(plastic_home, "store"), File.join(plastic_home, "projects")].map { |p| File.expand_path(p) }
1077
+ %w[store projects stores].map { |name| File.expand_path(File.join(plastic_home, name)) }
1024
1078
  end
1025
1079
 
1026
1080
  # `install_skills_flat` relocates any top-level underscore-prefixed markdown
@@ -1,4 +1,5 @@
1
1
  # encoding: UTF-8
2
+ require_relative "store_layout"
2
3
  # frozen_string_literal: true
3
4
  # IntentScreen (intent 316) - fills templates/intent-screen.md from one intent's
4
5
  # record: the intent file, the tier's INDEX.md, savepoint.md and checklist.md.
@@ -101,9 +102,8 @@ module IntentScreen
101
102
  # --- store ---------------------------------------------------------------------
102
103
 
103
104
  def self.store_fields(store_root)
104
- parent = File.basename(File.dirname(store_root))
105
- if parent == "projects"
106
- slug = File.basename(store_root)
105
+ slug = Plastic::StoreLayout.locate(File.join(store_root, "store")).last
106
+ if slug != Plastic::StoreLayout::GLOBAL
107
107
  { "store" => "project:#{slug}", "store.note" => "the #{slug} project store" }
108
108
  else
109
109
  { "store" => "global", "store.note" => "the global store" }
@@ -368,8 +368,8 @@ module Lock
368
368
  # file is inert: nothing reads it, it stays zero bytes, and
369
369
  # scripts/end-intent:558 keys its exit contract on Lock.path alone. This is
370
370
  # a requirement, not a guarantee the code enforces: the curator stray-file
371
- # rule (skills/conventions/references/maintenance-and-revisions.md:155)
372
- # has no *.lock carve-out today, so a wrongful delete there degrades one
371
+ # rule (docs/help/maintenance-and-revisions.md:155) has no *.lock
372
+ # carve-out today, so a wrongful delete there degrades one
373
373
  # write window to the pre-fix (unguarded) behavior, not a hard failure.
374
374
  def with_write_guard(intent_dir, type: "delivery",
375
375
  guard_timeout: WRITE_GUARD_TIMEOUT_SECONDS,
@@ -1,6 +1,7 @@
1
1
  # encoding: UTF-8
2
2
  # frozen_string_literal: true
3
3
 
4
+ require_relative "store_layout"
4
5
  require "yaml"
5
6
  require "date"
6
7
  require "digest"
@@ -70,7 +71,7 @@ module NodeInput
70
71
  status.success? ? truncate_landed_commits(out) : nil
71
72
  end
72
73
 
73
- PROJECT_LAYOUT_RE = %r{\A(.*)/projects/([^/]+)/store/[^/]+\z}.freeze
74
+ PROJECT_LAYOUT_RE = %r{\A(.*)/(?:projects|stores)/([^/]+)/store/[^/]+\z}.freeze
74
75
 
75
76
  # --- paths ---------------------------------------------------------------
76
77
 
@@ -399,7 +400,7 @@ module NodeInput
399
400
  return nil unless m
400
401
 
401
402
  home, slug = m[1], m[2]
402
- path = File.join(home, "projects", slug, "project.yml")
403
+ path = File.join(Plastic::StoreLayout.project_root(home, slug), "project.yml")
403
404
  return nil unless File.exist?(path)
404
405
 
405
406
  # `permitted_classes` (post-execution review finding B3): the same bug
@@ -1,7 +1,7 @@
1
1
  # encoding: UTF-8
2
2
  # frozen_string_literal: true
3
3
 
4
- require "rubygems"
4
+ require_relative "version_number"
5
5
 
6
6
  # Pure, dependency-injected pre-flight checks for Plastic's runtime dependencies
7
7
  # (intent 38). Takes injected probes (ruby version, node version, git presence,
@@ -14,9 +14,9 @@ require "rubygems"
14
14
  module Preflight
15
15
  module_function
16
16
 
17
- RUBY_FLOOR = "3.0.0"
17
+ RUBY_FLOOR = "4.0.0"
18
18
  NODE_FLOOR = 18
19
- RUBY_PIN = "3.3"
19
+ RUBY_PIN = "4.0"
20
20
 
21
21
  def check(ruby_version:, node_version:, git_present:, mise_present:)
22
22
  messages = []
@@ -63,9 +63,7 @@ module Preflight
63
63
  end
64
64
 
65
65
  def safe_version(str)
66
- Gem::Version.new(str.to_s)
67
- rescue ArgumentError
68
- nil
66
+ VersionNumber.parse(str)
69
67
  end
70
68
 
71
69
  def strip_leading_v(str)