@zalom/plastic 2.0.0-alpha.9 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (326) hide show
  1. package/PLASTIC.md +13 -136
  2. package/README.md +357 -133
  3. package/agents/plastic-enforcer.md +21 -16
  4. package/agents/plastic-executor.md +15 -4
  5. package/agents/plastic-node-research.md +30 -0
  6. package/agents/plastic-node-verify.md +28 -0
  7. package/agents/plastic-node-work.md +33 -0
  8. package/agents/{plastic-advisor.md → plastic-primary-advisor.md} +7 -9
  9. package/agents/{plastic-faux-advisor.md → plastic-secondary-advisor.md} +9 -12
  10. package/assets/plastic-logo.svg +1 -0
  11. package/bin/crap +4 -0
  12. package/bin/lib/context_budget.rb +43 -6
  13. package/bin/lib/skill_census.rb +839 -0
  14. package/bin/plastic +8 -0
  15. package/bin/plastic-skill-census +114 -0
  16. package/bin/test +24 -4
  17. package/bin/verify-change +345 -0
  18. package/config_asks.yml +4 -4
  19. package/deprecations.yml +1 -1
  20. package/{skills/agent-advisor/references → docs/help}/advisor-protocol.md +19 -24
  21. package/{skills/auto/references → docs/help}/agent-architecture.md +16 -14
  22. package/docs/help/completion-and-done.md +53 -0
  23. package/docs/help/human-report-contract.md +152 -0
  24. package/{skills/conventions/references → docs/help}/knowledge-graph.md +9 -0
  25. package/{skills/conventions/references → docs/help}/locks-and-worktrees.md +11 -13
  26. package/{skills/conventions/references → docs/help}/maintenance-and-revisions.md +1 -1
  27. package/{skills/conventions/references → docs/help}/roadmaps.md +5 -4
  28. package/{skills/tutorial/references → docs/help}/track-1-guided.md +28 -47
  29. package/{skills/tutorial/references → docs/help}/track-2-auto.md +8 -8
  30. package/{skills/tutorial/references → docs/help}/track-3-projects-and-roadmaps.md +24 -17
  31. package/hooks/call-budget +4 -0
  32. package/hooks/hooks.json +24 -0
  33. package/hooks/message-display +55 -2
  34. package/hooks/session-start +5 -1
  35. package/hooks/statusline +32 -27
  36. package/hooks/stop +5 -0
  37. package/package.json +5 -3
  38. package/scripts/append-ledger +2 -1
  39. package/scripts/dashboard.rb +267 -16
  40. package/scripts/day-summary +2 -1
  41. package/scripts/doctor.rb +680 -39
  42. package/scripts/end-intent +378 -56
  43. package/scripts/exec-worktree +5 -5
  44. package/scripts/file-session-intent +2 -1
  45. package/scripts/graph-measure +249 -0
  46. package/scripts/hook-call-budget +222 -0
  47. package/scripts/hook-capture +24 -123
  48. package/scripts/hook-close +2 -1
  49. package/scripts/hook-message-display +7 -0
  50. package/scripts/hook-record +24 -16
  51. package/scripts/hook-savepoint +27 -3
  52. package/scripts/hook-session-start +359 -321
  53. package/scripts/hook-stop +58 -0
  54. package/scripts/index-projection +74 -0
  55. package/scripts/insight-append +17 -4
  56. package/scripts/install.rb +9 -7
  57. package/scripts/lib/action_graph_shim.rb +279 -0
  58. package/scripts/lib/active_delivery.rb +105 -0
  59. package/scripts/lib/agent_models.rb +63 -25
  60. package/scripts/lib/arm.rb +74 -91
  61. package/scripts/lib/atomic_write.rb +31 -0
  62. package/scripts/lib/backup.rb +65 -0
  63. package/scripts/lib/cli/command.rb +92 -0
  64. package/scripts/lib/cli/commands/auto.rb +18 -0
  65. package/scripts/lib/cli/commands/auto_brief.rb +44 -0
  66. package/scripts/lib/cli/commands/auto_lock.rb +111 -0
  67. package/scripts/lib/cli/commands/auto_report.rb +50 -0
  68. package/scripts/lib/cli/commands/auto_take.rb +68 -0
  69. package/scripts/lib/cli/commands/backup.rb +43 -0
  70. package/scripts/lib/cli/commands/checkout.rb +25 -0
  71. package/scripts/lib/cli/commands/continue.rb +66 -0
  72. package/scripts/lib/cli/commands/doctor.rb +81 -0
  73. package/scripts/lib/cli/commands/feedback.rb +40 -0
  74. package/scripts/lib/cli/commands/help.rb +69 -0
  75. package/scripts/lib/cli/commands/hook.rb +32 -0
  76. package/scripts/lib/cli/commands/index.rb +23 -0
  77. package/scripts/lib/cli/commands/install.rb +21 -0
  78. package/scripts/lib/cli/commands/installer_verb.rb +37 -0
  79. package/scripts/lib/cli/commands/intent.rb +19 -0
  80. package/scripts/lib/cli/commands/intent_answer.rb +37 -0
  81. package/scripts/lib/cli/commands/intent_command.rb +58 -0
  82. package/scripts/lib/cli/commands/intent_end.rb +75 -0
  83. package/scripts/lib/cli/commands/intent_new.rb +62 -0
  84. package/scripts/lib/cli/commands/intent_note.rb +43 -0
  85. package/scripts/lib/cli/commands/intent_rule.rb +36 -0
  86. package/scripts/lib/cli/commands/intent_show.rb +32 -0
  87. package/scripts/lib/cli/commands/intent_spec.rb +44 -0
  88. package/scripts/lib/cli/commands/intent_step.rb +79 -0
  89. package/scripts/lib/cli/commands/intent_verify.rb +26 -0
  90. package/scripts/lib/cli/commands/migrate.rb +16 -0
  91. package/scripts/lib/cli/commands/migrate_stores.rb +31 -0
  92. package/scripts/lib/cli/commands/next.rb +54 -0
  93. package/scripts/lib/cli/commands/project.rb +19 -0
  94. package/scripts/lib/cli/commands/project_links.rb +46 -0
  95. package/scripts/lib/cli/commands/project_list.rb +20 -0
  96. package/scripts/lib/cli/commands/project_new.rb +95 -0
  97. package/scripts/lib/cli/commands/query.rb +31 -0
  98. package/scripts/lib/cli/commands/render.rb +35 -0
  99. package/scripts/lib/cli/commands/roadmap.rb +19 -0
  100. package/scripts/lib/cli/commands/roadmap_check.rb +54 -0
  101. package/scripts/lib/cli/commands/roadmap_log.rb +54 -0
  102. package/scripts/lib/cli/commands/roadmap_migrate.rb +53 -0
  103. package/scripts/lib/cli/commands/roadmap_next.rb +77 -0
  104. package/scripts/lib/cli/commands/roadmap_show.rb +65 -0
  105. package/scripts/lib/cli/commands/rollback.rb +20 -0
  106. package/scripts/lib/cli/commands/search.rb +60 -0
  107. package/scripts/lib/cli/commands/session.rb +18 -0
  108. package/scripts/lib/cli/commands/session_commit.rb +42 -0
  109. package/scripts/lib/cli/commands/session_handoff.rb +34 -0
  110. package/scripts/lib/cli/commands/session_summary.rb +35 -0
  111. package/scripts/lib/cli/commands/status.rb +68 -0
  112. package/scripts/lib/cli/commands/subcommand_list.rb +36 -0
  113. package/scripts/lib/cli/commands/sync.rb +55 -0
  114. package/scripts/lib/cli/commands/uninstall.rb +20 -0
  115. package/scripts/lib/cli/commands/update.rb +20 -0
  116. package/scripts/lib/cli/commands/version.rb +53 -0
  117. package/scripts/lib/cli/frontier.rb +86 -0
  118. package/scripts/lib/cli/intent_progress.rb +36 -0
  119. package/scripts/lib/cli/legacy.rb +80 -0
  120. package/scripts/lib/cli/output.rb +137 -0
  121. package/scripts/lib/cli/scope.rb +134 -0
  122. package/scripts/lib/cli/table.rb +65 -0
  123. package/scripts/lib/cli.rb +94 -0
  124. package/scripts/lib/codex_adapter.rb +198 -0
  125. package/scripts/lib/compact_instructions.rb +13 -5
  126. package/scripts/lib/core_integrity.rb +71 -0
  127. package/scripts/lib/dashboard_screen.rb +40 -0
  128. package/scripts/lib/data_boundary.rb +132 -0
  129. package/scripts/lib/day_summary.rb +23 -14
  130. package/scripts/lib/doctor_core.rb +114 -38
  131. package/scripts/lib/doctor_session_ledger.rb +5 -53
  132. package/scripts/lib/engine_permissions.rb +88 -0
  133. package/scripts/lib/exec_worktree.rb +24 -21
  134. package/scripts/lib/feedback_report.rb +1 -1
  135. package/scripts/lib/graph_edges.rb +137 -0
  136. package/scripts/lib/graph_file.rb +246 -0
  137. package/scripts/lib/graph_measure.rb +645 -0
  138. package/scripts/lib/graph_measure_budget.rb +409 -0
  139. package/scripts/lib/graph_measure_cohorts.rb +487 -0
  140. package/scripts/lib/graph_measure_models.rb +413 -0
  141. package/scripts/lib/graph_measure_report.rb +532 -0
  142. package/scripts/lib/graph_tree.rb +98 -0
  143. package/scripts/lib/guarded_append.rb +155 -0
  144. package/scripts/lib/handoff.rb +40 -13
  145. package/scripts/lib/harness_adapter.rb +184 -0
  146. package/scripts/lib/hook_registry.rb +27 -3
  147. package/scripts/lib/hook_replay.rb +235 -0
  148. package/scripts/lib/index_entry.rb +62 -0
  149. package/scripts/lib/index_projection.rb +201 -0
  150. package/scripts/lib/insights.rb +1 -1
  151. package/scripts/lib/installer_core.rb +543 -71
  152. package/scripts/lib/intent_screen.rb +4 -4
  153. package/scripts/lib/intent_screen_ansi.rb +73 -12
  154. package/scripts/lib/intent_validator.rb +2 -2
  155. package/scripts/lib/lock.rb +10 -11
  156. package/scripts/lib/message_display.rb +350 -54
  157. package/scripts/lib/meter_watch.rb +185 -0
  158. package/scripts/lib/node_file.rb +234 -0
  159. package/scripts/lib/node_ids.rb +99 -0
  160. package/scripts/lib/node_input.rb +921 -0
  161. package/scripts/lib/node_input_compatibility.rb +62 -0
  162. package/scripts/lib/node_ledger.rb +386 -0
  163. package/scripts/lib/node_progress.rb +153 -0
  164. package/scripts/lib/node_return.rb +204 -0
  165. package/scripts/lib/node_worktree.rb +337 -0
  166. package/scripts/lib/outcome_report.rb +440 -0
  167. package/scripts/lib/preflight.rb +4 -6
  168. package/scripts/lib/project_config.rb +46 -0
  169. package/scripts/lib/project_validator.rb +3 -2
  170. package/scripts/lib/qmd_sync.rb +8 -7
  171. package/scripts/lib/ready_set.rb +462 -0
  172. package/scripts/lib/reference_archive.rb +45 -0
  173. package/scripts/lib/release_guard.rb +18 -0
  174. package/scripts/lib/report_screen.rb +1371 -47
  175. package/scripts/lib/rlm/corpus.rb +13 -0
  176. package/scripts/lib/rlm/probe.rb +29 -0
  177. package/scripts/lib/rlm/query.rb +22 -0
  178. package/scripts/lib/roadmap_graph.rb +210 -0
  179. package/scripts/lib/roadmap_migration.rb +95 -0
  180. package/scripts/lib/roadmap_queue.rb +170 -12
  181. package/scripts/lib/roadmap_render.rb +150 -0
  182. package/scripts/lib/roadmap_savepoint.rb +64 -14
  183. package/scripts/lib/runner_absorb.rb +703 -0
  184. package/scripts/lib/runner_answer.rb +206 -0
  185. package/scripts/lib/runner_core.rb +194 -0
  186. package/scripts/lib/runner_dispatch.rb +526 -0
  187. package/scripts/lib/runner_policy.rb +191 -0
  188. package/scripts/lib/runner_proposals.rb +275 -0
  189. package/scripts/lib/runner_rewind.rb +201 -0
  190. package/scripts/lib/runner_sweep.rb +231 -0
  191. package/scripts/lib/runner_until_empty.rb +252 -0
  192. package/scripts/lib/runner_watch.rb +389 -0
  193. package/scripts/lib/savepoint.rb +141 -19
  194. package/scripts/lib/scaffold_intent.rb +6 -3
  195. package/scripts/lib/screen_paint.rb +365 -28
  196. package/scripts/lib/screens/dashboard.rb +20 -0
  197. package/scripts/lib/screens/plan.rb +18 -0
  198. package/scripts/lib/screens/roadmap.rb +15 -0
  199. package/scripts/lib/search_index.rb +55 -0
  200. package/scripts/lib/session_close.rb +30 -28
  201. package/scripts/lib/session_git.rb +67 -19
  202. package/scripts/lib/session_ledger.rb +48 -4
  203. package/scripts/lib/session_usage.rb +190 -0
  204. package/scripts/lib/sqlite.rb +22 -0
  205. package/scripts/lib/stop_gate.rb +95 -0
  206. package/scripts/lib/store_discovery.rb +7 -6
  207. package/scripts/lib/store_layout.rb +54 -0
  208. package/scripts/lib/store_provisioning.rb +2 -1
  209. package/scripts/lib/store_sync.rb +85 -0
  210. package/scripts/lib/stores_move.rb +93 -0
  211. package/scripts/lib/untouched_scaffold.rb +51 -0
  212. package/scripts/lib/verify_intent.rb +36 -8
  213. package/scripts/lib/version_number.rb +48 -0
  214. package/scripts/lib/work_graph.rb +59 -0
  215. package/scripts/lib/work_graph_validator.rb +201 -0
  216. package/scripts/lib/worktree.rb +27 -32
  217. package/scripts/lib/worktree_sweep.rb +6 -5
  218. package/scripts/link-suggest +2 -1
  219. package/scripts/meter-watch +57 -0
  220. package/scripts/migrate-to-global +1 -1
  221. package/scripts/new-intent +4 -13
  222. package/scripts/node-input +92 -0
  223. package/scripts/node-run +225 -0
  224. package/scripts/node-transition +291 -0
  225. package/scripts/outcome-report +74 -0
  226. package/scripts/plastic-lock +65 -60
  227. package/scripts/project-links +6 -21
  228. package/scripts/promote-session-item +3 -2
  229. package/scripts/read-config +53 -9
  230. package/scripts/ready-set +126 -0
  231. package/scripts/release-check +123 -0
  232. package/scripts/report-screen +177 -16
  233. package/scripts/roadmap-graph +125 -0
  234. package/scripts/roadmap-savepoint +7 -0
  235. package/scripts/rollback.rb +5 -1
  236. package/scripts/runner +581 -0
  237. package/scripts/savepoint-note +11 -9
  238. package/scripts/session-commit +2 -1
  239. package/scripts/session-usage +56 -0
  240. package/scripts/skill-lint +115 -6
  241. package/scripts/spawn-preamble +2 -2
  242. package/scripts/update.rb +31 -4
  243. package/scripts/validate-work-graph +39 -0
  244. package/scripts/verify-intent +3 -2
  245. package/scripts/write-handoff +2 -1
  246. package/templates/agents.md +7 -7
  247. package/templates/config.yml +16 -9
  248. package/templates/dashboard-screen.md +22 -0
  249. package/templates/display-fixture.md +21 -0
  250. package/templates/graph.md +16 -0
  251. package/templates/index.md +1 -1
  252. package/templates/intent-screen.md +1 -1
  253. package/templates/node-decision.md +11 -0
  254. package/templates/node-research.md +13 -0
  255. package/templates/node-verify.md +13 -0
  256. package/templates/node-work.md +22 -0
  257. package/templates/outcome.md +8 -3
  258. package/templates/project.yml +1 -1
  259. package/templates/render.css +10 -0
  260. package/templates/report-plan.md +15 -0
  261. package/templates/report-roadmap-delivered.md +10 -0
  262. package/templates/report-roadmap-plan.md +9 -0
  263. package/templates/report-roadmap-state.md +9 -0
  264. package/templates/report-state.md +1 -1
  265. package/templates/roadmap.md +13 -0
  266. package/bin/plastic.js +0 -70
  267. package/scripts/lib/bridge.rb +0 -116
  268. package/skills/agent-advisor/SKILL.md +0 -92
  269. package/skills/auto/SKILL.md +0 -296
  270. package/skills/auto/evals/evals.json +0 -255
  271. package/skills/auto/references/end-tail.md +0 -66
  272. package/skills/auto/references/human-report-contract.md +0 -78
  273. package/skills/conventions/SKILL.md +0 -29
  274. package/skills/conventions/references/completion-and-done.md +0 -43
  275. package/skills/dashboard/SKILL.md +0 -169
  276. package/skills/dashboard/evals/evals.json +0 -38
  277. package/skills/dashboard/references/classification.md +0 -22
  278. package/skills/dashboard/templates/dashboard-global.md +0 -20
  279. package/skills/dashboard/templates/dashboard-project.md +0 -19
  280. package/skills/direct/SKILL.md +0 -66
  281. package/skills/direct/references/request-signals.md +0 -59
  282. package/skills/doctor/SKILL.md +0 -299
  283. package/skills/doctor/report.md +0 -102
  284. package/skills/feedback/SKILL.md +0 -98
  285. package/skills/feedback/references/transport-and-privacy.md +0 -65
  286. package/skills/feedback/report.md +0 -36
  287. package/skills/install/SKILL.md +0 -217
  288. package/skills/intent-continuing/SKILL.md +0 -154
  289. package/skills/intent-continuing/references/board-fill.md +0 -43
  290. package/skills/intent-continuing/references/boarding-matrix.md +0 -34
  291. package/skills/intent-continuing/references/context-management.md +0 -28
  292. package/skills/intent-continuing/references/liveness-ranking.md +0 -57
  293. package/skills/intent-creating/SKILL.md +0 -164
  294. package/skills/intent-creating/evals/evals.json +0 -72
  295. package/skills/intent-creating/references/lifecycle.md +0 -81
  296. package/skills/intent-creating/references/wikilinks.md +0 -8
  297. package/skills/intent-ending/SKILL.md +0 -176
  298. package/skills/intent-ending/evals/evals.json +0 -74
  299. package/skills/intent-executing/SKILL.md +0 -170
  300. package/skills/intent-executing/evals/evals.json +0 -66
  301. package/skills/intent-executing/implementer-prompt.md +0 -42
  302. package/skills/intent-executing/spec-reviewer-prompt.md +0 -27
  303. package/skills/intent-speccing/SKILL.md +0 -130
  304. package/skills/intent-speccing/evals/evals.json +0 -126
  305. package/skills/intent-speccing/references/design-principles.md +0 -44
  306. package/skills/intent-speccing/references/per-section-fill-rules.md +0 -92
  307. package/skills/intent-speccing/references/self-verify-checklist.md +0 -37
  308. package/skills/project-creating/SKILL.md +0 -162
  309. package/skills/project-creating/references/hubs-projects.md +0 -55
  310. package/skills/project-creating/references/project-scaffolding.md +0 -97
  311. package/skills/releasing/SKILL.md +0 -337
  312. package/skills/releasing/references/deprecations.md +0 -60
  313. package/skills/releasing/references/promotion-and-tagging.md +0 -66
  314. package/skills/releasing/references/release-lines.md +0 -105
  315. package/skills/roadmap/SKILL.md +0 -64
  316. package/skills/roadmap/references/file-format.md +0 -124
  317. package/skills/roadmap/references/operations.md +0 -112
  318. package/skills/rollback/SKILL.md +0 -91
  319. package/skills/tutorial/SKILL.md +0 -65
  320. package/skills/tutorial/evals/evals.json +0 -186
  321. package/skills/uninstall/SKILL.md +0 -75
  322. package/skills/update/SKILL.md +0 -126
  323. /package/{skills/auto/references → docs/help}/agent-report-contract.md +0 -0
  324. /package/{skills/intent-executing → docs/help}/code-quality-reviewer-prompt.md +0 -0
  325. /package/{skills/conventions/references → docs/help}/lifecycle-and-savepoints.md +0 -0
  326. /package/{skills/intent-executing → docs/help}/plan-reviewer-prompt.md +0 -0
@@ -10,37 +10,48 @@
10
10
  module AgentModels
11
11
  # Claude Code aliases only (never pinned ids, never Fable). Keys are the agent
12
12
  # file basenames without the `.md` extension.
13
+ # The three per-kind node agents (intent 340b, G7c, n2), dispatched by
14
+ # HarnessAdapter::AGENT_TYPE_BY_KIND. work and research resolve the
15
+ # executor tier, verify the advisor tier, mirroring RunnerPolicy's
16
+ # model_role split (327 D12): work and research run on plastic-executor's
17
+ # own tier, and verify on the lifecycle advisor tier, never the cheap tier.
13
18
  TIER_DEFAULTS = {
14
19
  "plastic-enforcer" => "opus",
15
- "plastic-executor" => "sonnet"
20
+ "plastic-executor" => "sonnet",
21
+ "plastic-node-work" => "sonnet",
22
+ "plastic-node-verify" => "opus",
23
+ "plastic-node-research" => "sonnet"
16
24
  }.freeze
17
25
 
18
- # The two advisor agents (intent 185 final design): plastic-advisor (the real
19
- # advisor, ships `model: fable`) and plastic-faux-advisor (the imitation
20
- # advisor, an ordinary model carrying the same reasoning discipline inline,
21
- # ships `model: opus`). Both are shipped DEFAULTS in frontmatter, never a
26
+ # The two consultation agents are Primary Advisor and Secondary Advisor.
27
+ # Both ship Fable; Primary uses medium effort and Secondary uses high effort.
28
+ # Both are shipped defaults in frontmatter, never a
22
29
  # hard-wired identity: agents.models.claude.<name> (or the legacy flat form)
23
30
  # overrides either one through the same install-time frontmatter rewrite
24
31
  # every agent override uses. Neither is a lifecycle-stage role: never
25
- # dispatched by the auto pipeline, not part of TIER_DEFAULTS. Claude-only for
26
- # this release (generate_codex_agents skips both by name; the Codex advisor
27
- # case is intent 186, not a permanent exclusion).
28
- #
29
- # Intent 186 DEFINES the advisor Codex pairing but keeps emission deferred: when the skip is
30
- # lifted, plastic-advisor pairs to gpt-5.6-sol at xhigh (the deepest) and plastic-faux-advisor
31
- # to gpt-5.6-terra at high (cheaper). Neither is in TIER_DEFAULTS and neither is auto-dispatched.
32
- CONSULTATION_AGENTS = %w[plastic-advisor plastic-faux-advisor].freeze
33
-
34
- # Codex reasoning-effort per tier alias (intent 102a). model_reasoning_effort is a
35
- # depth-of-thinking dial independent of model selection (181 line 317-318), so mapping
36
- # the tier here never encodes a rotting Codex model id (116 D1). opus is the deepest
37
- # reasoning tier -> the deepest generally-safe effort (high, not the model-dependent
38
- # xhigh); sonnet the mid execution tier -> medium; haiku the lightest -> low. minimal is
39
- # unused.
32
+ # dispatched by the auto pipeline, not part of TIER_DEFAULTS. Codex installs
33
+ # generate both advisors with Astra and preserve each role's effort.
34
+ CONSULTATION_AGENTS = %w[plastic-primary-advisor plastic-secondary-advisor].freeze
35
+
36
+ SHIPPED_MODEL_DEFAULTS = TIER_DEFAULTS.merge(
37
+ "plastic-primary-advisor" => "fable",
38
+ "plastic-secondary-advisor" => "fable"
39
+ ).freeze
40
+
41
+ # Codex reasoning effort per tier alias. Model choice and reasoning effort are independent:
42
+ # aliases select the recommended OpenAI model, while every Plastic role starts at medium.
43
+ # A valid harness-scoped user override can still choose another effort for one agent.
40
44
  EFFORT_BY_ALIAS = {
41
- "opus" => "high",
45
+ "opus" => "medium",
42
46
  "sonnet" => "medium",
43
- "haiku" => "low"
47
+ "haiku" => "medium"
48
+ }.freeze
49
+
50
+ DEFAULT_EFFORT = "medium"
51
+
52
+ SHIPPED_EFFORT_DEFAULTS = {
53
+ "plastic-primary-advisor" => "medium",
54
+ "plastic-secondary-advisor" => "high"
44
55
  }.freeze
45
56
 
46
57
  # Codex model id per tier alias (intent 186). Codex has NO vendor alias layer: every model id
@@ -49,17 +60,26 @@ module AgentModels
49
60
  # centralizing every id in ONE map: Plastic owns the alias, so per-role identity costs a single
50
61
  # line to refresh on a Codex deprecation plus a Plastic release, and no per-role file carries a
51
62
  # raw id. opus (deepest reasoning tier) -> the flagship Sol; sonnet (execution tier) -> the
52
- # balanced Terra; haiku (lightest) -> the fast/cheap Luna. Paired with EFFORT_BY_ALIAS so
53
- # reasoning roles get a stronger model AND higher effort than executors. This is a shipped
54
- # DEFAULT, fully overridable via agents.models.codex.<name>.
63
+ # balanced Terra; haiku (lightest) -> the fast/cheap Luna. This is a shipped
64
+ # DEFAULT, fully overridable via agents.models.codex.<name>. Model strength does not change
65
+ # effort: all three aliases use medium unless agents.efforts.codex.<name> overrides it.
55
66
  CODEX_MODEL_BY_ALIAS = {
56
67
  "opus" => "gpt-5.6-sol",
57
68
  "sonnet" => "gpt-5.6-terra",
58
69
  "haiku" => "gpt-5.6-luna"
59
70
  }.freeze
60
71
 
72
+ CODEX_MODEL_BY_AGENT = {
73
+ "plastic-primary-advisor" => "gpt-6-astra",
74
+ "plastic-secondary-advisor" => "gpt-6-astra"
75
+ }.freeze
76
+
61
77
  module_function
62
78
 
79
+ def shipped_effort_for(agent)
80
+ SHIPPED_EFFORT_DEFAULTS.fetch(agent.to_s, DEFAULT_EFFORT)
81
+ end
82
+
63
83
  # Pull { basename => model } out of a loaded config hash's `agents.models`
64
84
  # section, scoped to `harness` ("claude" or "codex"), tolerating a missing or
65
85
  # malformed shape. `agents.models` can mix two shapes: legacy FLAT scalar
@@ -93,6 +113,17 @@ module AgentModels
93
113
  models_section(global_config, harness: harness).merge(models_section(project_config, harness: harness))
94
114
  end
95
115
 
116
+ def effort_override_map(project_config: {}, global_config: {}, harness: "claude")
117
+ efforts_section(global_config, harness).merge(efforts_section(project_config, harness))
118
+ end
119
+
120
+ def efforts_section(config, harness)
121
+ agents = config.is_a?(Hash) ? config["agents"] : nil
122
+ efforts = agents.is_a?(Hash) ? agents["efforts"] : nil
123
+ section = efforts.is_a?(Hash) ? efforts[harness] : nil
124
+ section.is_a?(Hash) ? section : {}
125
+ end
126
+
96
127
  # The model_reasoning_effort for a Plastic tier alias, or nil for any value that is not
97
128
  # one of the three shipped aliases (the caller treats nil as a literal Codex model id).
98
129
  def effort_for(value)
@@ -104,4 +135,11 @@ module AgentModels
104
135
  def codex_model_for(value)
105
136
  CODEX_MODEL_BY_ALIAS[value.to_s]
106
137
  end
138
+
139
+ def shipped_model_for(agent, harness: "claude")
140
+ value = SHIPPED_MODEL_DEFAULTS[agent.to_s]
141
+ return value unless harness.to_s == "codex"
142
+
143
+ CODEX_MODEL_BY_AGENT[agent.to_s] || codex_model_for(value) || value
144
+ end
107
145
  end
@@ -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 "digest"
5
6
  require "fileutils"
6
7
  require_relative "lock"
@@ -10,13 +11,14 @@ require_relative "savepoint"
10
11
 
11
12
  # Arm - how an auto team takes an intent and gives it back (intent 307).
12
13
  #
13
- # Three durable facts make a delivery: the delivery lock in the intent
14
- # directory (who owns it), the code worktree under the project repo (where
15
- # the code lands), and the per-session pointer in the global store's `.tmp/`
16
- # (which intent this session records into; a day id means the day ledger).
17
- # Before 2.0 a fourth thing, a `/tmp` bridge JSON, cached all three plus a
18
- # stage snapshot; ruling 6 of intent 296 retired it, and this module is what
19
- # replaced the bridge's arm, disarm, and repair methods (removed in 2.0, intent 307).
14
+ # Two durable facts make a delivery: the delivery lock in the intent
15
+ # directory says who delivers, and the code worktree under the project repo
16
+ # says where the code lands. A conversation session's tmp directory under
17
+ # the global store's `.tmp/` says only that a session started; it names no
18
+ # intent. This module carries the
19
+ # arm, disarm, and repair operations (intent 307); the pure INDEX/project-config
20
+ # helpers that once sat alongside them on a shared-helpers module now live on
21
+ # IndexEntry and ProjectConfig (intent 344).
20
22
  #
21
23
  # Pure and dependency-injected: every path and clock is an argument, every git
22
24
  # call goes through an injected runner, and the only environment read is the
@@ -32,7 +34,7 @@ module Arm
32
34
  end
33
35
 
34
36
  # Deterministic, session-less key derived from the store and the intent id,
35
- # for headless runs that carry no session id at all (moved from Bridge).
37
+ # for headless runs that carry no session id at all.
36
38
  def derive_key(store, intent_id)
37
39
  "auto-" + Digest::SHA256.hexdigest("#{store}/#{intent_id}")[0, 10]
38
40
  end
@@ -62,12 +64,12 @@ module Arm
62
64
  end
63
65
 
64
66
  def global_store(home)
65
- File.join(File.expand_path(home), ".plastic", "store")
67
+ Plastic::StoreLayout.global_store(File.join(File.expand_path(home), ".plastic"))
66
68
  end
67
69
 
68
70
  # The minimal hash Worktree.provision, release, and finish consume: the
69
71
  # intent block plus, when asked, the derived worktree block.
70
- def bridge_hash(intent_dir:, home: Dir.home, with_worktree: true)
72
+ def delivery(intent_dir:, home: Dir.home, with_worktree: true)
71
73
  dir = File.expand_path(intent_dir)
72
74
  data = {
73
75
  "intent" => { "id" => intent_id_for(dir), "dir" => File.basename(dir), "store" => store_for(dir) },
@@ -79,12 +81,7 @@ module Arm
79
81
  # `{code, code_branch, provisioned}` derived from projects.yml and the
80
82
  # intent id: the same path provision creates, `provisioned` iff it exists.
81
83
  def worktree_block(intent_dir:, home: Dir.home)
82
- dir = File.expand_path(intent_dir)
83
- store = store_for(dir)
84
- h = home_for(dir, home: home)
85
- slug = Worktree.slug_for_store(store, home: h)
86
- p = Worktree.paths(slug: slug, intent_id: intent_id_for(dir),
87
- intent_slug: Worktree.slug_from_dir(dir), home: h)
84
+ p = code_paths(intent_dir: intent_dir, home: home)
88
85
  code = p["code"]
89
86
  provisioned = !blank?(code) && Dir.exist?(code)
90
87
  {
@@ -94,78 +91,78 @@ module Arm
94
91
  }
95
92
  end
96
93
 
97
- # --- the pointer -------------------------------------------------------------
98
-
99
- def pointer_path(session, home:)
100
- store = global_store(home)
101
- SessionLedger.pointer_path(store, SessionLedger.short_session_id(nil, session))
102
- end
103
-
104
- def read_pointer(session, home:)
105
- path = pointer_path(session, home: home)
106
- File.exist?(path) ? File.read(path).strip : nil
94
+ # The code worktree path and branch this intent would have, whether or not
95
+ # the worktree exists on disk (blank code for a store-only project).
96
+ def code_paths(intent_dir:, home: Dir.home)
97
+ dir = File.expand_path(intent_dir)
98
+ h = home_for(dir, home: home)
99
+ slug = Worktree.slug_for_store(store_for(dir), home: h)
100
+ Worktree.paths(slug: slug, intent_id: intent_id_for(dir),
101
+ intent_slug: Worktree.slug_from_dir(dir), home: h)
107
102
  end
108
103
 
109
- def write_pointer(session, value, home:)
104
+ # Owner rule 2026-08-31: has this session already started a conversation?
105
+ # A conversation session's tmp directory is made at boot under the global
106
+ # store's `.tmp/`; a dispatched or headless session (a derived key) never
107
+ # gets one. Reads only; rescues to false (fail open: a broken store must
108
+ # never block a legitimate delivery).
109
+ def started_session?(session, home:)
110
110
  store = global_store(home)
111
- SessionLedger.ensure_tmp_root(store)
112
- path = pointer_path(session, home: home)
113
- FileUtils.mkdir_p(File.dirname(path))
114
- File.write(path, "#{value}\n")
115
- path
111
+ Dir.exist?(SessionLedger.session_tmp_dir(store, SessionLedger.short_session_id(nil, session)))
112
+ rescue StandardError
113
+ false
116
114
  end
117
115
 
118
116
  # --- arm ---------------------------------------------------------------------
119
117
 
120
118
  # Take an intent for `session`: acquire delivery.lock (stamped with the run
121
- # mode and the provenance the caller knows), provision the code worktree
122
- # (fail-open for a global-only or non-git intent), and point the session at
123
- # the intent. Returns `{status:, lock:, worktree:, session:, pointer:}`.
124
- # A held, stale, excluded, or corrupt lock returns that status with the
125
- # lock data read and touches nothing.
119
+ # mode and the provenance the caller knows) and provision the code worktree
120
+ # (fail-open for a global-only or non-git intent). Returns
121
+ # `{status:, lock:, worktree:, session:}`. A held, stale, excluded, or
122
+ # corrupt lock returns that status with the lock data read and touches
123
+ # nothing.
126
124
  def arm(intent_dir:, session:, mode: "auto", home: Dir.home, harness: nil,
127
125
  agent: nil, model: nil, thread: nil, now: Time.now, runner: Worktree::ShellRunner.new,
128
- host: Socket.gethostname)
126
+ host: Socket.gethostname, allow_inline: false)
129
127
  raise ArgumentError, "mode must be auto or guided" unless %w[auto guided].include?(mode.to_s)
130
128
  dir = File.expand_path(intent_dir)
131
129
  key = resolve_session(session, store: store_for(dir), intent_id: intent_id_for(dir))
132
130
  h = home_for(dir, home: home)
133
131
 
132
+ # Owner rule 2026-08-31: the main session never delivers an intent inline.
133
+ # A conversation session (its tmp directory already exists under the
134
+ # global store) arming an intent it does not already hold the lock for is
135
+ # inline delivery and is refused BEFORE any lock is taken. Re-arming the
136
+ # intent it already holds stays idempotent. A dispatched or headless
137
+ # session has no tmp directory and arms freely. --allow-inline is the
138
+ # explicit owner override.
139
+ if !allow_inline && started_session?(key, home: h) && !Lock.holds?(dir, session: key)
140
+ return { status: :inline_refused, lock: nil, worktree: nil, session: key }
141
+ end
142
+
134
143
  status, lock = Lock.acquire(dir, session: key, host: host, now: now,
135
144
  harness: harness, agent: agent, model: model,
136
145
  thread: thread, run_mode: mode.to_s)
137
146
  unless %i[acquired owned].include?(status)
138
- return { status: status, lock: lock, worktree: nil, session: key, pointer: nil }
147
+ return { status: status, lock: lock, worktree: nil, session: key }
139
148
  end
140
149
 
141
- data = bridge_hash(intent_dir: dir, home: h, with_worktree: false)
150
+ data = delivery(intent_dir: dir, home: h, with_worktree: false)
142
151
  begin
143
152
  Worktree.provision(data, home: h, runner: runner)
144
153
  rescue StandardError => e
145
154
  warn "plastic: worktree provision raised, continuing unprovisioned: #{e.message}"
146
155
  end
147
156
 
148
- # The pointer is what the capture and record hooks read; they key on the
149
- # harness's real session id, so a derived key gets no pointer (nothing would
150
- # ever read it). A pointer failure warns and never undoes an acquired lock.
151
- pointer = nil
152
- unless key == derive_key(store_for(dir), intent_id_for(dir))
153
- begin
154
- pointer = write_pointer(key, intent_id_for(dir), home: h)
155
- rescue StandardError => e
156
- warn "plastic: session pointer not written (#{e.message}); the lock is held regardless"
157
- end
158
- end
159
157
  { status: status, lock: lock, worktree: worktree_block(intent_dir: dir, home: h),
160
- session: key, pointer: pointer }
158
+ session: key }
161
159
  end
162
160
 
163
161
  # --- disarm ------------------------------------------------------------------
164
162
 
165
- # Give the intent back: remove the worktree (when `remove`), release the
166
- # lock as its recorded owner (falling back to `session`), and reset the
167
- # session pointer to today's day id when it named this intent. Returns
168
- # the lock release status (:released, :none, :not_owner, or :raised).
163
+ # Give the intent back: remove the worktree (when `remove`) and release the
164
+ # lock as its recorded owner (falling back to `session`). Returns the lock
165
+ # release status (:released, :none, :not_owner, or :raised).
169
166
  def disarm(intent_dir:, session:, home: Dir.home, runner: Worktree::ShellRunner.new,
170
167
  remove: true, now: Time.now)
171
168
  dir = File.expand_path(intent_dir)
@@ -173,45 +170,36 @@ module Arm
173
170
  key = resolve_session(session, store: store_for(dir), intent_id: intent_id_for(dir))
174
171
 
175
172
  begin
176
- Worktree.release(bridge_hash(intent_dir: dir, home: h), home: h, runner: runner, remove: remove)
173
+ Worktree.release(delivery(intent_dir: dir, home: h), home: h, runner: runner, remove: remove)
177
174
  rescue StandardError => e
178
175
  warn "plastic: worktree release raised, continuing: #{e.message}"
179
176
  end
180
177
 
181
178
  lock = Lock.read(dir)
182
179
  owner = lock && !blank?(lock["owner_session"]) ? lock["owner_session"] : key
183
- release_status = begin
180
+ begin
184
181
  Lock.release(dir, session: owner)
185
182
  rescue StandardError => e
186
183
  warn "plastic: delivery lock release raised for #{dir}, continuing: #{e.message}"
187
184
  :raised
188
185
  end
189
-
190
- begin
191
- reset_pointer(key, intent_id_for(dir), home: h, now: now)
192
- rescue StandardError => e
193
- warn "plastic: session pointer not reset (#{e.message})"
194
- end
195
- release_status
196
- end
197
-
198
- # Point the session back at the day ledger, but only when it names `intent_id`.
199
- def reset_pointer(session, intent_id, home:, now: Time.now)
200
- current = read_pointer(session, home: home)
201
- return false unless current == intent_id.to_s
202
- write_pointer(session, SessionLedger.day_id(now), home: home)
203
- true
204
186
  end
205
187
 
206
188
  # --- repair ------------------------------------------------------------------
207
189
 
208
- # One idempotent repair (the lock half of the bridge's repair, removed in 2.0):
190
+ # A stale foreign lock is reclaimed only by an audited owner step, never by
191
+ # the session that found it.
192
+ def stale_hint(intent_id)
193
+ "reclaiming a stale lock is the owner's audited step; plastic auto lock status #{intent_id} shows it"
194
+ end
195
+
196
+ # One idempotent repair (the lock half of the repair path):
209
197
  # remove a corrupt lock, back off from a fresh foreign lock (`held`), report
210
198
  # a stale foreign lock (`stale`) for the explicit reclaim verb, keep and
211
199
  # enrich an own lock, heartbeat a delegated one, acquire when none, and
212
200
  # provision the worktree so the repaired intent has its checkout.
213
201
  def repair(intent_dir:, session:, home: Dir.home, now: Time.now, harness: nil,
214
- agent: nil, model: nil, thread: nil, run_mode: nil, hint_harness: nil,
202
+ agent: nil, model: nil, thread: nil, run_mode: nil,
215
203
  runner: Worktree::ShellRunner.new)
216
204
  dir = File.expand_path(intent_dir)
217
205
  h = home_for(dir, home: home)
@@ -231,11 +219,18 @@ module Arm
231
219
  end
232
220
  return { "status" => "stale", "owner" => lock["owner_session"],
233
221
  "actions" => actions, "session" => key,
234
- "hint" => "run #{Lock.skill_ref('plastic-doctor', harness: hint_harness || harness)} " \
235
- "reclaim the lock to take over with an audit" }
222
+ "hint" => stale_hint(intent_id_for(dir)) }
236
223
  end
237
224
 
238
- mode = blank?(run_mode) ? (lock && lock["run_mode"]) : run_mode.to_s
225
+ # A lock rebuilt from nothing (none, or a corrupt one removed) is an auto
226
+ # delivery's lock again; a kept lock keeps whatever mode it had.
227
+ mode = if !blank?(run_mode)
228
+ run_mode.to_s
229
+ elsif lock
230
+ lock["run_mode"]
231
+ else
232
+ "auto"
233
+ end
239
234
  if lock
240
235
  if lock["owner_session"].to_s == key.to_s
241
236
  lock_data = lock.dup
@@ -259,7 +254,7 @@ module Arm
259
254
  end
260
255
 
261
256
  begin
262
- Worktree.provision(bridge_hash(intent_dir: dir, home: h, with_worktree: false), home: h, runner: runner)
257
+ Worktree.provision(delivery(intent_dir: dir, home: h, with_worktree: false), home: h, runner: runner)
263
258
  rescue StandardError => e
264
259
  warn "plastic: worktree provision raised during repair, continuing unprovisioned: #{e.message}"
265
260
  end
@@ -268,16 +263,4 @@ module Arm
268
263
 
269
264
  { "status" => "repaired", "actions" => actions, "lock" => lock_data, "session" => key }
270
265
  end
271
-
272
- # The intent directory this session's pointer names, searched in the given
273
- # stores in order; nil when the pointer is absent, empty, or a day id.
274
- def intent_dir_from_pointer(session, home:, stores:)
275
- current = read_pointer(session, home: home)
276
- return nil if blank?(current) || SessionLedger.valid_day_id?(current)
277
- stores.each do |store|
278
- match = Dir.glob(File.join(store, "#{current}--*")).find { |p| File.directory?(p) }
279
- return match if match
280
- end
281
- nil
282
- end
283
266
  end
@@ -0,0 +1,31 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
4
+ # AtomicWrite (intent 334, n2, D19r): a sibling-temp-plus-rename write, the
5
+ # same shape as Lock#write (scripts/lib/lock.rb) - a content write is never
6
+ # an in-place truncate, so a crash mid-write can never leave an empty or
7
+ # half-written target on disk. The temp file is a SIBLING in the same
8
+ # directory as the target, never a system tmpdir, because File.rename can
9
+ # raise EXDEV when the temp and the target live on different filesystems.
10
+ # The temp name ends in .lock (post-execution review, non-blocking 5), the
11
+ # same property Lock#write_temp_path documents: an orphan left by a crash
12
+ # between the write and the rename is covered by the store's existing
13
+ # *.lock gitignore rule rather than swept into its git add -A auto-commit.
14
+ #
15
+ # The renamer is injectable so the interrupted-write case (a rename that
16
+ # raises) is testable with dependency injection, never eval or a global
17
+ # (D19r).
18
+ module AtomicWrite
19
+ module_function
20
+
21
+ def write(path, content, renamer: File.method(:rename))
22
+ dir = File.dirname(path)
23
+ temp = File.join(dir, ".#{File.basename(path)}.tmp.#{Process.pid}.#{Time.now.to_f}.#{rand(0xFFFFFF)}.lock")
24
+ File.write(temp, content)
25
+ renamer.call(temp, path)
26
+ true
27
+ rescue StandardError
28
+ File.delete(temp) if temp && File.exist?(temp)
29
+ raise
30
+ end
31
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "digest"
5
+ require "json"
6
+ require "tmpdir"
7
+ require "zlib"
8
+ require_relative "store_sync"
9
+ require_relative "worktree"
10
+
11
+ module Plastic
12
+ module Backup
13
+ DATABASES = [SearchIndex::NAME, WorkGraph::NAME, ReferenceArchive::NAME].freeze
14
+ ROOT_FILES = %w[config.yml projects.yml INDEX.md].freeze
15
+ Missing = Class.new(StandardError)
16
+
17
+ def self.dir(home)
18
+ File.join(home, "backups")
19
+ end
20
+
21
+ def self.list(home)
22
+ Dir.glob(File.join(dir(home), "plastic-backup--*.tar.gz")).sort
23
+ end
24
+
25
+ def self.call(home, version:, now: Time.now.utc)
26
+ missing = DATABASES.reject { |name| File.exist?(File.join(home, name)) }
27
+ raise Missing, missing.join(", ") unless missing.empty?
28
+
29
+ Worktree.ensure_gitignored(home, "backups/")
30
+ Dir.mktmpdir("plastic-backup") do |work|
31
+ DATABASES.each { |name| Sqlite.call(File.join(home, name), "VACUUM INTO #{Sqlite.quote(File.join(work, name))};", readonly: true) }
32
+ FileUtils.cp(ROOT_FILES.map { |name| File.join(home, name) }.select { |path| File.exist?(path) }, work)
33
+ File.write(File.join(work, "manifest.json"), JSON.pretty_generate(manifest(work, version, now)))
34
+ pack(work, File.join(dir(home), "plastic-backup--#{now.strftime("%Y%m%dT%H%M%SZ")}.tar.gz"))
35
+ end
36
+ end
37
+
38
+ def self.manifest(work, version, now)
39
+ files = Dir.children(work).sort.map do |name|
40
+ path = File.join(work, name)
41
+ {"name" => name, "bytes" => File.size(path), "sha256" => Digest::SHA256.file(path).hexdigest}
42
+ end
43
+ {"version" => version, "at" => now.strftime("%Y-%m-%dT%H:%M:%SZ"), "files" => files}
44
+ end
45
+
46
+ def self.pack(work, archive)
47
+ require "rubygems"
48
+ require "rubygems/package"
49
+ FileUtils.mkdir_p(File.dirname(archive))
50
+ draft = "#{archive}.tmp"
51
+ Zlib::GzipWriter.open(draft) do |gzip|
52
+ Gem::Package::TarWriter.new(gzip) do |tar|
53
+ Dir.children(work).sort.each do |name|
54
+ path = File.join(work, name)
55
+ tar.add_file_simple(name, 0o644, File.size(path)) { |entry| IO.copy_stream(path, entry) }
56
+ end
57
+ end
58
+ end
59
+ File.rename(draft, archive)
60
+ archive
61
+ ensure
62
+ FileUtils.rm_f(draft.to_s)
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "optparse"
4
+ require_relative "output"
5
+ require_relative "scope"
6
+
7
+ module Plastic
8
+ class CLI
9
+ class Command
10
+ OK = 0
11
+ FAILED = 1
12
+ USAGE = 2
13
+ REFUSED = 3
14
+
15
+ Usage = Class.new(StandardError)
16
+ Refusal = Class.new(StandardError)
17
+ Failure = Class.new(StandardError)
18
+
19
+ def self.call(...)
20
+ command = new(...)
21
+ command.validate_scope
22
+ command.call
23
+ command.flush
24
+ OK
25
+ rescue OptionParser::ParseError, Scope::UnknownProject, Usage => e
26
+ command.output.usage(e.message, self::USAGE_LINE)
27
+ USAGE
28
+ rescue Refusal => e
29
+ command.output.refused(e.message)
30
+ REFUSED
31
+ rescue Failure => e
32
+ command.output.failed(e.message)
33
+ FAILED
34
+ end
35
+
36
+ attr_reader :output
37
+
38
+ def initialize(argv, out:, err:, env: ENV, home: Dir.home, directory: Dir.pwd)
39
+ @argv = argv.dup
40
+ @env = env
41
+ @home = home
42
+ @directory = directory
43
+ @output = Output.new(out: out, err: err, json: @argv.include?("--json"))
44
+ end
45
+
46
+ def call
47
+ raise NoMethodError, "#{self.class} must define call"
48
+ end
49
+
50
+ def validate_scope
51
+ scope.slug if @argv.grep(/\A--project(?:=|$)/).any?
52
+ end
53
+
54
+ def flush
55
+ @output.project = scope.slug
56
+ @output.flush(json: options[:json])
57
+ end
58
+
59
+ private
60
+
61
+ def options
62
+ return @options if @options
63
+
64
+ @options = {}
65
+ parser.parse!(@argv)
66
+ @options
67
+ end
68
+
69
+ def arguments
70
+ options
71
+ @argv
72
+ end
73
+
74
+ def parser
75
+ @parser ||= OptionParser.new do |o|
76
+ o.banner = self.class::USAGE_LINE
77
+ o.on("--json") { @options[:json] = true }
78
+ o.on("--project SLUG") { |slug| @options[:project] = slug }
79
+ switches(o)
80
+ end
81
+ end
82
+
83
+ def switches(_parser)
84
+ nil
85
+ end
86
+
87
+ def scope
88
+ @scope ||= Scope.new(env: @env, home: @home, slug: options[:project], directory: @directory)
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "subcommand_list"
4
+
5
+ # `plastic auto` - the list of `plastic auto ...` subcommands, straight out of
6
+ # the table.
7
+ module Plastic
8
+ class CLI
9
+ module Commands
10
+ class Auto < SubcommandList
11
+ USAGE_LINE = "plastic auto SUBCOMMAND [options]"
12
+ PREFIX = "auto"
13
+ NEXT = "plastic auto take ID"
14
+ BECAUSE = "taking the intent is what every other auto subcommand needs first"
15
+ end
16
+ end
17
+ end
18
+ end