@zalom/plastic 2.0.0-alpha.26 → 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 (224) hide show
  1. package/PLASTIC.md +13 -139
  2. package/README.md +337 -133
  3. package/agents/plastic-enforcer.md +12 -12
  4. package/agents/plastic-executor.md +2 -1
  5. package/agents/plastic-node-research.md +4 -2
  6. package/agents/plastic-node-verify.md +1 -0
  7. package/agents/plastic-node-work.md +1 -0
  8. package/agents/{plastic-advisor.md → plastic-primary-advisor.md} +6 -8
  9. package/agents/{plastic-faux-advisor.md → plastic-secondary-advisor.md} +9 -12
  10. package/bin/crap +4 -0
  11. package/bin/lib/context_budget.rb +35 -1
  12. package/bin/lib/skill_census.rb +839 -0
  13. package/bin/plastic +6 -0
  14. package/bin/plastic-skill-census +114 -0
  15. package/bin/verify-change +345 -0
  16. package/config_asks.yml +4 -4
  17. package/deprecations.yml +1 -1
  18. package/{skills/agent-advisor/references → docs/help}/advisor-protocol.md +19 -24
  19. package/{skills/auto/references → docs/help}/agent-architecture.md +7 -7
  20. package/{skills/conventions/references → docs/help}/completion-and-done.md +1 -1
  21. package/{skills/auto/references → docs/help}/human-report-contract.md +17 -19
  22. package/{skills/conventions/references → docs/help}/roadmaps.md +2 -2
  23. package/{skills/tutorial/references → docs/help}/track-1-guided.md +8 -8
  24. package/{skills/tutorial/references → docs/help}/track-2-auto.md +5 -5
  25. package/{skills/tutorial/references → docs/help}/track-3-projects-and-roadmaps.md +24 -17
  26. package/package.json +3 -2
  27. package/scripts/append-ledger +2 -1
  28. package/scripts/dashboard.rb +10 -9
  29. package/scripts/day-summary +2 -1
  30. package/scripts/doctor.rb +57 -49
  31. package/scripts/end-intent +2 -8
  32. package/scripts/file-session-intent +2 -1
  33. package/scripts/hook-capture +4 -3
  34. package/scripts/hook-close +2 -1
  35. package/scripts/hook-record +3 -2
  36. package/scripts/hook-savepoint +3 -2
  37. package/scripts/hook-session-start +5 -4
  38. package/scripts/hook-stop +2 -1
  39. package/scripts/insight-append +1 -2
  40. package/scripts/install.rb +7 -5
  41. package/scripts/lib/action_graph_shim.rb +3 -1
  42. package/scripts/lib/active_delivery.rb +1 -1
  43. package/scripts/lib/agent_models.rb +44 -26
  44. package/scripts/lib/arm.rb +2 -1
  45. package/scripts/lib/backup.rb +63 -0
  46. package/scripts/lib/cli/command.rb +85 -0
  47. package/scripts/lib/cli/commands/auto.rb +18 -0
  48. package/scripts/lib/cli/commands/auto_brief.rb +44 -0
  49. package/scripts/lib/cli/commands/auto_lock.rb +60 -0
  50. package/scripts/lib/cli/commands/auto_report.rb +50 -0
  51. package/scripts/lib/cli/commands/auto_take.rb +25 -0
  52. package/scripts/lib/cli/commands/backup.rb +43 -0
  53. package/scripts/lib/cli/commands/checkout.rb +25 -0
  54. package/scripts/lib/cli/commands/continue.rb +66 -0
  55. package/scripts/lib/cli/commands/doctor.rb +20 -0
  56. package/scripts/lib/cli/commands/feedback.rb +40 -0
  57. package/scripts/lib/cli/commands/help.rb +69 -0
  58. package/scripts/lib/cli/commands/hook.rb +32 -0
  59. package/scripts/lib/cli/commands/index.rb +23 -0
  60. package/scripts/lib/cli/commands/install.rb +21 -0
  61. package/scripts/lib/cli/commands/installer_verb.rb +37 -0
  62. package/scripts/lib/cli/commands/intent.rb +19 -0
  63. package/scripts/lib/cli/commands/intent_answer.rb +37 -0
  64. package/scripts/lib/cli/commands/intent_command.rb +53 -0
  65. package/scripts/lib/cli/commands/intent_end.rb +61 -0
  66. package/scripts/lib/cli/commands/intent_new.rb +62 -0
  67. package/scripts/lib/cli/commands/intent_note.rb +43 -0
  68. package/scripts/lib/cli/commands/intent_rule.rb +36 -0
  69. package/scripts/lib/cli/commands/intent_show.rb +25 -0
  70. package/scripts/lib/cli/commands/intent_spec.rb +44 -0
  71. package/scripts/lib/cli/commands/intent_step.rb +43 -0
  72. package/scripts/lib/cli/commands/intent_verify.rb +26 -0
  73. package/scripts/lib/cli/commands/migrate.rb +16 -0
  74. package/scripts/lib/cli/commands/migrate_stores.rb +31 -0
  75. package/scripts/lib/cli/commands/next.rb +49 -0
  76. package/scripts/lib/cli/commands/project.rb +19 -0
  77. package/scripts/lib/cli/commands/project_links.rb +42 -0
  78. package/scripts/lib/cli/commands/project_list.rb +20 -0
  79. package/scripts/lib/cli/commands/project_new.rb +72 -0
  80. package/scripts/lib/cli/commands/query.rb +31 -0
  81. package/scripts/lib/cli/commands/render.rb +25 -0
  82. package/scripts/lib/cli/commands/roadmap.rb +19 -0
  83. package/scripts/lib/cli/commands/roadmap_check.rb +44 -0
  84. package/scripts/lib/cli/commands/roadmap_log.rb +54 -0
  85. package/scripts/lib/cli/commands/roadmap_next.rb +34 -0
  86. package/scripts/lib/cli/commands/roadmap_show.rb +45 -0
  87. package/scripts/lib/cli/commands/rollback.rb +20 -0
  88. package/scripts/lib/cli/commands/search.rb +60 -0
  89. package/scripts/lib/cli/commands/session.rb +18 -0
  90. package/scripts/lib/cli/commands/session_commit.rb +42 -0
  91. package/scripts/lib/cli/commands/session_handoff.rb +34 -0
  92. package/scripts/lib/cli/commands/session_summary.rb +35 -0
  93. package/scripts/lib/cli/commands/status.rb +68 -0
  94. package/scripts/lib/cli/commands/subcommand_list.rb +36 -0
  95. package/scripts/lib/cli/commands/sync.rb +46 -0
  96. package/scripts/lib/cli/commands/uninstall.rb +20 -0
  97. package/scripts/lib/cli/commands/update.rb +20 -0
  98. package/scripts/lib/cli/commands/version.rb +53 -0
  99. package/scripts/lib/cli/frontier.rb +84 -0
  100. package/scripts/lib/cli/legacy.rb +50 -0
  101. package/scripts/lib/cli/output.rb +102 -0
  102. package/scripts/lib/cli/scope.rb +127 -0
  103. package/scripts/lib/cli/table.rb +64 -0
  104. package/scripts/lib/cli.rb +94 -0
  105. package/scripts/lib/codex_adapter.rb +5 -4
  106. package/scripts/lib/compact_instructions.rb +8 -0
  107. package/scripts/lib/day_summary.rb +4 -3
  108. package/scripts/lib/doctor_core.rb +7 -32
  109. package/scripts/lib/doctor_session_ledger.rb +2 -1
  110. package/scripts/lib/feedback_report.rb +1 -1
  111. package/scripts/lib/graph_measure_models.rb +4 -2
  112. package/scripts/lib/index_entry.rb +9 -0
  113. package/scripts/lib/installer_core.rb +175 -56
  114. package/scripts/lib/intent_screen.rb +3 -3
  115. package/scripts/lib/lock.rb +2 -2
  116. package/scripts/lib/node_file.rb +22 -2
  117. package/scripts/lib/node_input.rb +11 -6
  118. package/scripts/lib/node_return.rb +8 -3
  119. package/scripts/lib/preflight.rb +2 -4
  120. package/scripts/lib/project_config.rb +2 -1
  121. package/scripts/lib/project_validator.rb +3 -2
  122. package/scripts/lib/qmd_sync.rb +8 -7
  123. package/scripts/lib/reference_archive.rb +45 -0
  124. package/scripts/lib/release_guard.rb +2 -0
  125. package/scripts/lib/report_screen.rb +4 -3
  126. package/scripts/lib/rlm/corpus.rb +13 -0
  127. package/scripts/lib/rlm/probe.rb +29 -0
  128. package/scripts/lib/rlm/query.rb +22 -0
  129. package/scripts/lib/roadmap_queue.rb +2 -2
  130. package/scripts/lib/roadmap_savepoint.rb +1 -1
  131. package/scripts/lib/runner_absorb.rb +59 -2
  132. package/scripts/lib/runner_dispatch.rb +12 -8
  133. package/scripts/lib/runner_policy.rb +28 -10
  134. package/scripts/lib/search_index.rb +55 -0
  135. package/scripts/lib/session_git.rb +4 -3
  136. package/scripts/lib/sqlite.rb +22 -0
  137. package/scripts/lib/store_discovery.rb +7 -6
  138. package/scripts/lib/store_layout.rb +54 -0
  139. package/scripts/lib/store_provisioning.rb +2 -1
  140. package/scripts/lib/store_sync.rb +85 -0
  141. package/scripts/lib/stores_move.rb +93 -0
  142. package/scripts/lib/verify_intent.rb +2 -7
  143. package/scripts/lib/version_number.rb +48 -0
  144. package/scripts/lib/work_graph.rb +59 -0
  145. package/scripts/lib/worktree.rb +3 -8
  146. package/scripts/lib/worktree_sweep.rb +3 -2
  147. package/scripts/link-suggest +2 -1
  148. package/scripts/migrate-to-global +1 -1
  149. package/scripts/new-intent +3 -12
  150. package/scripts/node-run +2 -1
  151. package/scripts/plastic-lock +3 -2
  152. package/scripts/promote-session-item +3 -2
  153. package/scripts/read-config +44 -6
  154. package/scripts/release-check +10 -5
  155. package/scripts/report-screen +1 -1
  156. package/scripts/session-commit +2 -1
  157. package/scripts/spawn-preamble +2 -2
  158. package/scripts/update.rb +25 -4
  159. package/scripts/write-handoff +2 -1
  160. package/templates/agents.md +6 -6
  161. package/templates/config.yml +13 -6
  162. package/templates/node-research.md +4 -2
  163. package/templates/render.css +10 -0
  164. package/bin/plastic.js +0 -70
  165. package/skills/agent-advisor/SKILL.md +0 -92
  166. package/skills/auto/SKILL.md +0 -297
  167. package/skills/auto/evals/evals.json +0 -255
  168. package/skills/auto/references/end-tail.md +0 -64
  169. package/skills/conventions/SKILL.md +0 -29
  170. package/skills/dashboard/SKILL.md +0 -180
  171. package/skills/dashboard/evals/evals.json +0 -38
  172. package/skills/dashboard/references/classification.md +0 -22
  173. package/skills/dashboard/templates/dashboard-global.md +0 -20
  174. package/skills/dashboard/templates/dashboard-project.md +0 -19
  175. package/skills/direct/SKILL.md +0 -66
  176. package/skills/direct/references/request-signals.md +0 -59
  177. package/skills/doctor/SKILL.md +0 -305
  178. package/skills/doctor/report.md +0 -102
  179. package/skills/feedback/SKILL.md +0 -98
  180. package/skills/feedback/references/transport-and-privacy.md +0 -65
  181. package/skills/feedback/report.md +0 -36
  182. package/skills/install/SKILL.md +0 -217
  183. package/skills/intent-continuing/SKILL.md +0 -156
  184. package/skills/intent-continuing/references/board-fill.md +0 -52
  185. package/skills/intent-continuing/references/boarding-matrix.md +0 -35
  186. package/skills/intent-continuing/references/context-management.md +0 -28
  187. package/skills/intent-continuing/references/liveness-ranking.md +0 -57
  188. package/skills/intent-creating/SKILL.md +0 -89
  189. package/skills/intent-creating/evals/evals.json +0 -72
  190. package/skills/intent-creating/references/lifecycle.md +0 -81
  191. package/skills/intent-creating/references/wikilinks.md +0 -8
  192. package/skills/intent-ending/SKILL.md +0 -182
  193. package/skills/intent-ending/evals/evals.json +0 -74
  194. package/skills/intent-executing/SKILL.md +0 -87
  195. package/skills/intent-executing/evals/evals.json +0 -66
  196. package/skills/intent-executing/implementer-prompt.md +0 -47
  197. package/skills/intent-executing/spec-reviewer-prompt.md +0 -27
  198. package/skills/intent-speccing/SKILL.md +0 -136
  199. package/skills/intent-speccing/evals/evals.json +0 -126
  200. package/skills/intent-speccing/references/design-principles.md +0 -44
  201. package/skills/intent-speccing/references/per-section-fill-rules.md +0 -92
  202. package/skills/intent-speccing/references/self-verify-checklist.md +0 -37
  203. package/skills/project-creating/SKILL.md +0 -162
  204. package/skills/project-creating/references/hubs-projects.md +0 -55
  205. package/skills/project-creating/references/project-scaffolding.md +0 -97
  206. package/skills/releasing/SKILL.md +0 -376
  207. package/skills/releasing/references/deprecations.md +0 -60
  208. package/skills/releasing/references/promotion-and-tagging.md +0 -70
  209. package/skills/releasing/references/release-lines.md +0 -105
  210. package/skills/roadmap/SKILL.md +0 -90
  211. package/skills/roadmap/references/file-format.md +0 -134
  212. package/skills/roadmap/references/operations.md +0 -112
  213. package/skills/rollback/SKILL.md +0 -91
  214. package/skills/tutorial/SKILL.md +0 -66
  215. package/skills/tutorial/evals/evals.json +0 -186
  216. package/skills/uninstall/SKILL.md +0 -75
  217. package/skills/update/SKILL.md +0 -126
  218. /package/{skills/auto/references → docs/help}/agent-report-contract.md +0 -0
  219. /package/{skills/intent-executing → docs/help}/code-quality-reviewer-prompt.md +0 -0
  220. /package/{skills/conventions/references → docs/help}/knowledge-graph.md +0 -0
  221. /package/{skills/conventions/references → docs/help}/lifecycle-and-savepoints.md +0 -0
  222. /package/{skills/conventions/references → docs/help}/locks-and-worktrees.md +0 -0
  223. /package/{skills/conventions/references → docs/help}/maintenance-and-revisions.md +0 -0
  224. /package/{skills/intent-executing → docs/help}/plan-reviewer-prompt.md +0 -0
@@ -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
@@ -1132,7 +1186,12 @@ class InstallerCore
1132
1186
  # Codex-scoped overrides only (agents.models.codex.*): a literal Claude
1133
1187
  # model id set under agents.models.claude.* (or the legacy flat form,
1134
1188
  # which resolves as claude) must never reach a Codex TOML.
1135
- installed += generate_codex_agents(File.join(config[:home_dir], "agents"), models: agent_model_overrides(harness: "codex"))
1189
+ installed += generate_codex_agents(
1190
+ File.join(config[:home_dir], "agents"),
1191
+ models: agent_model_overrides(harness: "codex"),
1192
+ efforts: agent_effort_overrides(harness: "codex"),
1193
+ advisor_enabled: advisor_enabled?
1194
+ )
1136
1195
 
1137
1196
  # Instruction injection (L1): Plastic standing conventions into ~/.codex/AGENTS.md.
1138
1197
  # Partial-ownership file, so it is NOT manifest-tracked (stripped surgically on uninstall).
@@ -1162,24 +1221,17 @@ class InstallerCore
1162
1221
  # a whole-file, Plastic-owned .toml per repo agents/*.md instead of copying markdown.
1163
1222
  # The returned paths append to `installed`, so they are manifest-tracked and pruned on
1164
1223
  # uninstall by the manifest whole-file-delete path, exactly like ~/.claude/agents/*.md.
1165
- def generate_codex_agents(agents_root, models: {})
1224
+ def generate_codex_agents(agents_root, models: {}, efforts: {}, advisor_enabled: true)
1166
1225
  sources = Dir.glob(File.join(package_root, "agents", "*.md"))
1167
1226
  return [] if sources.empty?
1168
1227
 
1169
1228
  FileUtils.mkdir_p(agents_root)
1170
1229
  sources.filter_map do |src|
1171
1230
  basename = File.basename(src, ".md")
1172
- # Codex advisor support is out of scope for this release (intent 185): the
1173
- # owner has not evaluated the Codex reasoning-model ecosystem long enough to
1174
- # judge it. Skip every AgentModels::CONSULTATION_AGENTS file (both
1175
- # plastic-advisor and plastic-faux-advisor) by name, a deliberate and
1176
- # mechanical scope cut tracked at intent 186 (Codex advisor evaluation), not
1177
- # a permanent exclusion and not conditioned on any frontmatter or override
1178
- # value.
1179
- next if AgentModels::CONSULTATION_AGENTS.include?(basename)
1231
+ next if !advisor_enabled && AgentModels::CONSULTATION_AGENTS.include?(basename)
1180
1232
 
1181
1233
  dest = File.join(agents_root, "#{basename}.toml")
1182
- write_text_atomic(dest, render_codex_agent_toml(src, models[basename]))
1234
+ write_text_atomic(dest, render_codex_agent_toml(src, models[basename], efforts[basename]))
1183
1235
  dest
1184
1236
  end
1185
1237
  end
@@ -1187,16 +1239,25 @@ class InstallerCore
1187
1239
  # Render one repo agents/*.md into a deterministic Codex agent TOML document. Fixed field
1188
1240
  # order (name, description, the model field(s) from codex_model_fields, developer_instructions)
1189
1241
  # so regenerate is byte-identical (idempotency).
1190
- def render_codex_agent_toml(source_path, override)
1242
+ def render_codex_agent_toml(source_path, override, effort_override = nil)
1191
1243
  front, body = split_frontmatter(File.read(source_path))
1192
1244
  name = (front["name"] || File.basename(source_path, ".md")).to_s
1193
1245
  description = (front["description"] || "").to_s
1194
- effective = (override && !override.to_s.empty? ? override : front["model"]).to_s
1246
+ effective = if override && !override.to_s.empty?
1247
+ override
1248
+ else
1249
+ AgentModels.shipped_model_for(name, harness: "codex") || front["model"]
1250
+ end
1251
+ effort = if effort_override && !effort_override.to_s.empty?
1252
+ effort_override
1253
+ else
1254
+ front["effort"] || AgentModels.shipped_effort_for(name)
1255
+ end
1195
1256
 
1196
1257
  parts = []
1197
1258
  parts << %(name = "#{toml_inline_escape(name)}")
1198
1259
  parts << %(description = "#{toml_inline_escape(description)}")
1199
- parts << codex_model_fields(effective)
1260
+ parts << codex_model_fields(effective, effort: effort)
1200
1261
  parts << "developer_instructions = \"\"\"\n#{toml_ml_escape(body.strip)}\n\"\"\""
1201
1262
  parts.reject(&:empty?).join("\n") + "\n"
1202
1263
  end
@@ -1216,20 +1277,20 @@ class InstallerCore
1216
1277
  # The model-selection line(s). A known tier alias (opus/sonnet/haiku) emits BOTH a `model` line
1217
1278
  # (from AgentModels.codex_model_for, the intent-186 per-role Codex identity) and a
1218
1279
  # model_reasoning_effort line, model first for deterministic byte-identical regenerate. Any other
1219
- # non-empty value is a literal Codex model id emitted verbatim as `model` only. Empty -> no line
1220
- # (the agent inherits the session default). If an alias somehow lacks a mapped model, the effort
1221
- # line still emits alone (backward-safe).
1222
- def codex_model_fields(effective)
1280
+ # non-empty value is a literal Codex model id. Every non-empty model also emits the resolved effort,
1281
+ # medium by default or the harness-scoped override. Empty emits no model fields.
1282
+ def codex_model_fields(effective, effort: AgentModels::DEFAULT_EFFORT)
1223
1283
  return "" if effective.nil? || effective.to_s.empty?
1224
- effort = AgentModels.effort_for(effective)
1225
- if effort
1284
+ if AgentModels.effort_for(effective)
1226
1285
  lines = []
1227
1286
  model = AgentModels.codex_model_for(effective)
1228
1287
  lines << %(model = "#{toml_inline_escape(model)}") if model && !model.to_s.empty?
1229
- lines << %(model_reasoning_effort = "#{effort}")
1288
+ lines << %(model_reasoning_effort = "#{toml_inline_escape(effort)}") unless effort.to_s.empty?
1230
1289
  lines.join("\n")
1231
1290
  else
1232
- %(model = "#{toml_inline_escape(effective.to_s)}")
1291
+ lines = [%(model = "#{toml_inline_escape(effective.to_s)}")]
1292
+ lines << %(model_reasoning_effort = "#{toml_inline_escape(effort)}") unless effort.to_s.empty?
1293
+ lines.join("\n")
1233
1294
  end
1234
1295
  end
1235
1296
 
@@ -1362,8 +1423,7 @@ class InstallerCore
1362
1423
  # them to `installed` before write_manifest (manifest + prune are then automatic).
1363
1424
  # No-op safe: returns [] when the package has no agents dir or it is empty.
1364
1425
  # advisor_enabled: false (advisor.enabled config key) skips every
1365
- # AgentModels::CONSULTATION_AGENTS file entirely (both plastic-advisor and
1366
- # plastic-faux-advisor), so a user who declined the advisor never gets either
1426
+ # AgentModels::CONSULTATION_AGENTS file entirely, so a user who declined the advisor never gets either
1367
1427
  # agent installed.
1368
1428
  def install_agents(agents_root, models: {}, efforts: {}, advisor_enabled: true)
1369
1429
  sources = Dir.glob(File.join(package_root, "agents", "*.md"))
@@ -1408,18 +1468,21 @@ class InstallerCore
1408
1468
  # ("claude" or "codex"). Defaults are NOT included, so unconfigured agents
1409
1469
  # keep their shipped frontmatter.
1410
1470
  #
1411
- # Both advisor agents (plastic-advisor, plastic-faux-advisor) resolve through
1471
+ # Both advisor agents resolve through
1412
1472
  # this SAME generic map, like any other agent: a config author sets
1413
- # agents.models.claude.plastic-advisor (or the legacy flat
1414
- # agents.models.plastic-advisor, read as claude) to point either agent at a
1473
+ # agents.models.claude.<agent> (or the legacy flat form, read as Claude) to point either agent at a
1415
1474
  # different literal model. There is no separate advisor-specific model key;
1416
1475
  # which agent the advisor SKILL routes to by default is a routing decision
1417
1476
  # (advisor.claude.default), never a model-selection one.
1418
1477
  def agent_model_overrides(project_dir = nil, harness: "claude")
1419
- global_config = load_config_yaml(File.join(plastic_home, "config.yml"))
1478
+ global_path = File.join(plastic_home, "config.yml")
1479
+ migrate_advisor_config_file(global_path)
1480
+ global_config = load_config_yaml(global_path)
1420
1481
  project_config =
1421
1482
  if project_dir
1422
- load_config_yaml(File.join(project_dir, ".plastic_store", "config.yml"))
1483
+ project_path = File.join(project_dir, ".plastic_store", "config.yml")
1484
+ migrate_advisor_config_file(project_path)
1485
+ load_config_yaml(project_path)
1423
1486
  else
1424
1487
  {}
1425
1488
  end
@@ -1427,8 +1490,12 @@ class InstallerCore
1427
1490
  end
1428
1491
 
1429
1492
  def agent_effort_overrides(project_dir = nil, harness: "claude")
1430
- global_config = load_config_yaml(File.join(plastic_home, "config.yml"))
1431
- project_config = project_dir ? load_config_yaml(File.join(project_dir, ".plastic_store", "config.yml")) : {}
1493
+ global_path = File.join(plastic_home, "config.yml")
1494
+ migrate_advisor_config_file(global_path)
1495
+ global_config = load_config_yaml(global_path)
1496
+ project_path = project_dir && File.join(project_dir, ".plastic_store", "config.yml")
1497
+ migrate_advisor_config_file(project_path) if project_path
1498
+ project_config = project_path ? load_config_yaml(project_path) : {}
1432
1499
  AgentModels.effort_override_map(project_config: project_config, global_config: global_config, harness: harness)
1433
1500
  end
1434
1501
 
@@ -1443,10 +1510,14 @@ class InstallerCore
1443
1510
  value != false
1444
1511
  end
1445
1512
 
1446
- # Agent-name shorthands for the --advisor flag: the two shipped choices,
1447
- # named for the role (real advisor vs. the cheaper imitation), never a model
1448
- # name.
1449
- ADVISOR_SHORTHANDS = { "real" => "plastic-advisor", "faux" => "plastic-faux-advisor" }.freeze
1513
+ ADVISOR_NAME_MIGRATIONS = {
1514
+ "plastic-advisor" => "plastic-primary-advisor",
1515
+ "plastic-faux-advisor" => "plastic-secondary-advisor"
1516
+ }.freeze
1517
+ ADVISOR_SHORTHANDS = {
1518
+ "primary" => "plastic-primary-advisor", "secondary" => "plastic-secondary-advisor",
1519
+ "real" => "plastic-primary-advisor", "faux" => "plastic-secondary-advisor"
1520
+ }.freeze
1450
1521
 
1451
1522
  # Write advisor.enabled / advisor.claude.default into the global config.yml
1452
1523
  # from install-time flags. Absent flags change nothing: advisor.enabled
@@ -1454,7 +1525,7 @@ class InstallerCore
1454
1525
  # (the skill's own fallback chain applies) when missing.
1455
1526
  # --no-advisor -> advisor.enabled: false
1456
1527
  # --advisor VALUE -> advisor.claude.default: VALUE (an agent name, or the
1457
- # shorthand "real"/"faux")
1528
+ # shorthand "primary"/"secondary")
1458
1529
  def apply_config_flags(argv)
1459
1530
  no_advisor = argv.include?("--no-advisor")
1460
1531
  advisor_idx = argv.index("--advisor")
@@ -1462,7 +1533,13 @@ class InstallerCore
1462
1533
  return unless no_advisor || advisor_value
1463
1534
 
1464
1535
  config_path = File.join(plastic_home, "config.yml")
1465
- config = load_config_yaml(config_path)
1536
+ config = if File.exist?(config_path)
1537
+ parsed = YAML.safe_load(File.read(config_path))
1538
+ return false unless parsed.nil? || parsed.is_a?(Hash)
1539
+ migrate_advisor_config(parsed || {})
1540
+ else
1541
+ {}
1542
+ end
1466
1543
 
1467
1544
  if no_advisor
1468
1545
  config["advisor"] ||= {}
@@ -1477,6 +1554,9 @@ class InstallerCore
1477
1554
 
1478
1555
  FileUtils.mkdir_p(plastic_home)
1479
1556
  File.write(config_path, YAML.dump(config))
1557
+ true
1558
+ rescue StandardError
1559
+ false
1480
1560
  end
1481
1561
 
1482
1562
  def load_config_yaml(path)
@@ -1486,6 +1566,45 @@ class InstallerCore
1486
1566
  {}
1487
1567
  end
1488
1568
 
1569
+ # Renames retired advisor keys without discarding a current key. It accepts
1570
+ # malformed config sections and leaves unrelated values untouched.
1571
+ def migrate_advisor_config(config)
1572
+ return {} unless config.is_a?(Hash)
1573
+ config = Marshal.load(Marshal.dump(config))
1574
+ advisor = config["advisor"]
1575
+ if advisor.is_a?(Hash) && advisor["claude"].is_a?(Hash)
1576
+ default = advisor["claude"]["default"]
1577
+ advisor["claude"]["default"] = ADVISOR_NAME_MIGRATIONS.fetch(default, default)
1578
+ end
1579
+ agents = config["agents"]
1580
+ %w[models efforts].each do |section_name|
1581
+ section = agents.is_a?(Hash) ? agents[section_name] : nil
1582
+ next unless section.is_a?(Hash)
1583
+ migrate_advisor_keys!(section)
1584
+ %w[claude codex].each { |harness| migrate_advisor_keys!(section[harness]) if section[harness].is_a?(Hash) }
1585
+ end
1586
+ config
1587
+ end
1588
+
1589
+ def migrate_advisor_config_file(path)
1590
+ return false unless path && File.file?(path)
1591
+ parsed = YAML.safe_load(File.read(path))
1592
+ return false unless parsed.is_a?(Hash)
1593
+ migrated = migrate_advisor_config(parsed)
1594
+ return false if migrated == parsed
1595
+ File.write(path, YAML.dump(migrated))
1596
+ true
1597
+ rescue StandardError
1598
+ false
1599
+ end
1600
+
1601
+ def migrate_advisor_keys!(section)
1602
+ ADVISOR_NAME_MIGRATIONS.each do |legacy, current|
1603
+ section[current] = section[legacy] if !section.key?(current) && section.key?(legacy)
1604
+ section.delete(legacy)
1605
+ end
1606
+ end
1607
+
1489
1608
  # --- Legacy plugin migration ---
1490
1609
 
1491
1610
  # Earlier versions registered Plastic as a local marketplace plugin
@@ -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,
@@ -5,7 +5,8 @@ require "yaml"
5
5
  require "date"
6
6
 
7
7
  # NodeFile (intent 334, n3): one node file's YAML envelope (`node`, `kind`,
8
- # `files`, `budget`) over a Markdown body, plus the deterministic id minter
8
+ # `files`, `budget`, and the optional research `report`) over a Markdown body,
9
+ # plus the deterministic id minter
9
10
  # (327 D1r, D5r, D9r-D12r, D16r). The kind-prefix rule lives here, not in
10
11
  # GraphEdges, which stays loose about id grammar so a numeric roadmap id
11
12
  # parses the same way (D12r).
@@ -83,6 +84,8 @@ module NodeFile
83
84
 
84
85
  budget, budget_errors = normalize_budget(fm["budget"])
85
86
  errors.concat(budget_errors)
87
+ report, report_errors = normalize_report(fm["report"], kind)
88
+ errors.concat(report_errors)
86
89
 
87
90
  {
88
91
  ok: errors.empty?,
@@ -90,15 +93,32 @@ module NodeFile
90
93
  kind: kind.empty? ? nil : kind,
91
94
  files: files.is_a?(Array) ? files : nil,
92
95
  budget: budget,
96
+ report: report,
93
97
  body: body,
94
98
  errors: errors,
95
99
  }
96
100
  end
97
101
 
98
102
  def failure(errors)
99
- { ok: false, node: nil, kind: nil, files: nil, budget: nil, body: nil, errors: errors }
103
+ { ok: false, node: nil, kind: nil, files: nil, budget: nil, report: nil, body: nil, errors: errors }
100
104
  end
101
105
 
106
+ def normalize_report(raw, kind)
107
+ return [nil, []] if raw.nil?
108
+ return [nil, ["report: is allowed only on research nodes"]] unless kind == "research"
109
+ return [nil, ["report: must be a non-empty relative Markdown path under resources/"]] unless raw.is_a?(String)
110
+
111
+ path = raw
112
+ parts = path.split("/", -1)
113
+ valid = path == path.strip && parts.length >= 2 && parts.first == "resources" &&
114
+ parts.none? { |part| part.empty? || %w[. ..].include?(part) } &&
115
+ !path.include?("\\") && path.end_with?(".md")
116
+ return [nil, ["report: must be a relative .md path under resources/ with no traversal"]] unless valid
117
+
118
+ [path, []]
119
+ end
120
+ private_class_method :normalize_report
121
+
102
122
  # <id>.md or <id>--<slug>.md, exactly - a longer id's file (n11--x.md) must
103
123
  # never satisfy a shorter id (n1), so the id is matched as the whole prefix
104
124
  # up to end-of-string or the literal "--" separator, never as a substring
@@ -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
 
@@ -118,22 +119,26 @@ module NodeInput
118
119
  end
119
120
 
120
121
  text = render_node_block(node: node, kind: parsed[:kind], files: parsed[:files], budget: parsed[:budget],
121
- body: parsed[:body])
122
+ report: parsed[:report], body: parsed[:body])
122
123
  { ok: true, error_kind: nil, text: text, errors: [], kind: parsed[:kind], files: parsed[:files] || [],
123
- budget: parsed[:budget] }
124
+ budget: parsed[:budget], report: parsed[:report] }
124
125
  end
125
126
 
126
127
  def failure_block(kind, errors)
127
- { ok: false, error_kind: kind, text: nil, errors: errors, kind: nil, files: nil, budget: nil }
128
+ { ok: false, error_kind: kind, text: nil, errors: errors, kind: nil, files: nil, budget: nil, report: nil }
128
129
  end
129
130
  private_class_method :failure_block
130
131
 
131
- def render_node_block(node:, kind:, files:, budget:, body:)
132
+ def render_node_block(node:, kind:, files:, budget:, body:, report: nil)
132
133
  lines = []
133
134
  lines << "# Node #{node}"
134
135
  lines << "kind: #{kind}"
135
136
  lines << "files: #{Array(files).join(', ')}"
136
137
  lines << "budget: #{budget}"
138
+ if report
139
+ lines << "report: #{report}"
140
+ lines << "report delivery: return the complete Markdown body in the YAML report field; the runner writes it"
141
+ end
137
142
  lines << ""
138
143
  lines << body.to_s.strip
139
144
  "#{lines.join("\n")}\n"
@@ -395,7 +400,7 @@ module NodeInput
395
400
  return nil unless m
396
401
 
397
402
  home, slug = m[1], m[2]
398
- path = File.join(home, "projects", slug, "project.yml")
403
+ path = File.join(Plastic::StoreLayout.project_root(home, slug), "project.yml")
399
404
  return nil unless File.exist?(path)
400
405
 
401
406
  # `permitted_classes` (post-execution review finding B3): the same bug
@@ -33,7 +33,7 @@ module NodeReturn
33
33
  STATUSES = %w[done failed_verification needs_decision blocked].freeze
34
34
 
35
35
  ALLOWED_KEYS = %w[
36
- node status commit summary findings proposed_nodes proposed_edges question reason
36
+ node status commit summary findings report proposed_nodes proposed_edges question reason
37
37
  ].freeze
38
38
 
39
39
  REQUIRED_FIELDS = {
@@ -51,7 +51,7 @@ module NodeReturn
51
51
 
52
52
  Result = Struct.new(
53
53
  :ok, :node, :status, :commit, :summary, :findings, :proposed_nodes, :proposed_edges,
54
- :question, :reason, :errors,
54
+ :report, :question, :reason, :errors,
55
55
  keyword_init: true
56
56
  )
57
57
 
@@ -86,6 +86,10 @@ module NodeReturn
86
86
  proposed_edges, edge_errors = normalize_proposed_edges(doc["proposed_edges"])
87
87
  return failure(edge_errors) if edge_errors.any?
88
88
 
89
+ if !doc["report"].nil? && !doc["report"].is_a?(String)
90
+ return failure(["report: must be a Markdown string"])
91
+ end
92
+
89
93
  Result.new(
90
94
  ok: true,
91
95
  node: doc["node"].to_s,
@@ -95,6 +99,7 @@ module NodeReturn
95
99
  findings: normalize_findings(doc["findings"]),
96
100
  proposed_nodes: proposed_nodes,
97
101
  proposed_edges: proposed_edges,
102
+ report: doc["report"],
98
103
  question: doc["question"],
99
104
  reason: doc["reason"],
100
105
  errors: [],
@@ -111,7 +116,7 @@ module NodeReturn
111
116
  def failure(errors)
112
117
  Result.new(
113
118
  ok: false, node: nil, status: nil, commit: nil, summary: nil, findings: [],
114
- proposed_nodes: [], proposed_edges: [], question: nil, reason: nil,
119
+ proposed_nodes: [], proposed_edges: [], report: nil, question: nil, reason: nil,
115
120
  errors: Array(errors)
116
121
  )
117
122
  end
@@ -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,
@@ -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)
@@ -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
 
6
7
  # ProjectConfig - reads a project's project.yml merged over the shipped defaults
@@ -18,7 +19,7 @@ module ProjectConfig
18
19
  }.freeze
19
20
 
20
21
  def read(slug)
21
- path = File.join(Dir.home, ".plastic", "projects", slug, "project.yml")
22
+ path = File.join(Plastic::StoreLayout.project_root(File.join(Dir.home, ".plastic"), slug), "project.yml")
22
23
  config = if File.exist?(path)
23
24
  YAML.safe_load(File.read(path)) || {}
24
25
  else