@zalom/plastic 2.0.0-alpha.9 → 2.0.1

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 (322) hide show
  1. package/PLASTIC.md +13 -136
  2. package/README.md +357 -133
  3. package/agents/plastic-enforcer.md +20 -15
  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 +6 -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/{skills/conventions/references → docs/help}/completion-and-done.md +15 -16
  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 +2 -2
  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 +153 -30
  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 +48 -83
  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 +99 -0
  67. package/scripts/lib/cli/commands/auto_report.rb +50 -0
  68. package/scripts/lib/cli/commands/auto_take.rb +31 -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 +53 -0
  82. package/scripts/lib/cli/commands/intent_end.rb +62 -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 +71 -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 +42 -0
  95. package/scripts/lib/cli/commands/project_list.rb +20 -0
  96. package/scripts/lib/cli/commands/project_new.rb +72 -0
  97. package/scripts/lib/cli/commands/query.rb +31 -0
  98. package/scripts/lib/cli/commands/render.rb +33 -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 +70 -0
  104. package/scripts/lib/cli/commands/roadmap_show.rb +45 -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 +46 -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 +229 -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 +531 -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 +913 -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 +158 -8
  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 +525 -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 +25 -18
  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/verify_intent.rb +36 -8
  212. package/scripts/lib/version_number.rb +48 -0
  213. package/scripts/lib/work_graph.rb +59 -0
  214. package/scripts/lib/work_graph_validator.rb +201 -0
  215. package/scripts/lib/worktree.rb +27 -32
  216. package/scripts/lib/worktree_sweep.rb +6 -5
  217. package/scripts/link-suggest +2 -1
  218. package/scripts/meter-watch +57 -0
  219. package/scripts/migrate-to-global +1 -1
  220. package/scripts/new-intent +4 -13
  221. package/scripts/node-input +92 -0
  222. package/scripts/node-run +225 -0
  223. package/scripts/node-transition +291 -0
  224. package/scripts/outcome-report +74 -0
  225. package/scripts/plastic-lock +42 -33
  226. package/scripts/promote-session-item +3 -2
  227. package/scripts/read-config +53 -9
  228. package/scripts/ready-set +126 -0
  229. package/scripts/release-check +123 -0
  230. package/scripts/report-screen +177 -16
  231. package/scripts/roadmap-graph +119 -0
  232. package/scripts/roadmap-savepoint +7 -0
  233. package/scripts/runner +581 -0
  234. package/scripts/savepoint-note +11 -9
  235. package/scripts/session-commit +2 -1
  236. package/scripts/session-usage +56 -0
  237. package/scripts/skill-lint +115 -6
  238. package/scripts/spawn-preamble +2 -2
  239. package/scripts/update.rb +25 -4
  240. package/scripts/validate-work-graph +39 -0
  241. package/scripts/verify-intent +3 -2
  242. package/scripts/write-handoff +2 -1
  243. package/templates/agents.md +7 -7
  244. package/templates/config.yml +16 -9
  245. package/templates/dashboard-screen.md +22 -0
  246. package/templates/display-fixture.md +21 -0
  247. package/templates/graph.md +16 -0
  248. package/templates/index.md +1 -1
  249. package/templates/intent-screen.md +1 -1
  250. package/templates/node-decision.md +11 -0
  251. package/templates/node-research.md +13 -0
  252. package/templates/node-verify.md +13 -0
  253. package/templates/node-work.md +22 -0
  254. package/templates/outcome.md +8 -3
  255. package/templates/project.yml +1 -1
  256. package/templates/render.css +10 -0
  257. package/templates/report-plan.md +15 -0
  258. package/templates/report-roadmap-delivered.md +10 -0
  259. package/templates/report-roadmap-plan.md +9 -0
  260. package/templates/report-roadmap-state.md +9 -0
  261. package/templates/report-state.md +1 -1
  262. package/templates/roadmap.md +13 -0
  263. package/bin/plastic.js +0 -70
  264. package/scripts/lib/bridge.rb +0 -116
  265. package/skills/agent-advisor/SKILL.md +0 -92
  266. package/skills/auto/SKILL.md +0 -296
  267. package/skills/auto/evals/evals.json +0 -255
  268. package/skills/auto/references/end-tail.md +0 -66
  269. package/skills/auto/references/human-report-contract.md +0 -78
  270. package/skills/conventions/SKILL.md +0 -29
  271. package/skills/dashboard/SKILL.md +0 -169
  272. package/skills/dashboard/evals/evals.json +0 -38
  273. package/skills/dashboard/references/classification.md +0 -22
  274. package/skills/dashboard/templates/dashboard-global.md +0 -20
  275. package/skills/dashboard/templates/dashboard-project.md +0 -19
  276. package/skills/direct/SKILL.md +0 -66
  277. package/skills/direct/references/request-signals.md +0 -59
  278. package/skills/doctor/SKILL.md +0 -299
  279. package/skills/doctor/report.md +0 -102
  280. package/skills/feedback/SKILL.md +0 -98
  281. package/skills/feedback/references/transport-and-privacy.md +0 -65
  282. package/skills/feedback/report.md +0 -36
  283. package/skills/install/SKILL.md +0 -217
  284. package/skills/intent-continuing/SKILL.md +0 -154
  285. package/skills/intent-continuing/references/board-fill.md +0 -43
  286. package/skills/intent-continuing/references/boarding-matrix.md +0 -34
  287. package/skills/intent-continuing/references/context-management.md +0 -28
  288. package/skills/intent-continuing/references/liveness-ranking.md +0 -57
  289. package/skills/intent-creating/SKILL.md +0 -164
  290. package/skills/intent-creating/evals/evals.json +0 -72
  291. package/skills/intent-creating/references/lifecycle.md +0 -81
  292. package/skills/intent-creating/references/wikilinks.md +0 -8
  293. package/skills/intent-ending/SKILL.md +0 -176
  294. package/skills/intent-ending/evals/evals.json +0 -74
  295. package/skills/intent-executing/SKILL.md +0 -170
  296. package/skills/intent-executing/evals/evals.json +0 -66
  297. package/skills/intent-executing/implementer-prompt.md +0 -42
  298. package/skills/intent-executing/spec-reviewer-prompt.md +0 -27
  299. package/skills/intent-speccing/SKILL.md +0 -130
  300. package/skills/intent-speccing/evals/evals.json +0 -126
  301. package/skills/intent-speccing/references/design-principles.md +0 -44
  302. package/skills/intent-speccing/references/per-section-fill-rules.md +0 -92
  303. package/skills/intent-speccing/references/self-verify-checklist.md +0 -37
  304. package/skills/project-creating/SKILL.md +0 -162
  305. package/skills/project-creating/references/hubs-projects.md +0 -55
  306. package/skills/project-creating/references/project-scaffolding.md +0 -97
  307. package/skills/releasing/SKILL.md +0 -337
  308. package/skills/releasing/references/deprecations.md +0 -60
  309. package/skills/releasing/references/promotion-and-tagging.md +0 -66
  310. package/skills/releasing/references/release-lines.md +0 -105
  311. package/skills/roadmap/SKILL.md +0 -64
  312. package/skills/roadmap/references/file-format.md +0 -124
  313. package/skills/roadmap/references/operations.md +0 -112
  314. package/skills/rollback/SKILL.md +0 -91
  315. package/skills/tutorial/SKILL.md +0 -65
  316. package/skills/tutorial/evals/evals.json +0 -186
  317. package/skills/uninstall/SKILL.md +0 -75
  318. package/skills/update/SKILL.md +0 -126
  319. /package/{skills/auto/references → docs/help}/agent-report-contract.md +0 -0
  320. /package/{skills/intent-executing → docs/help}/code-quality-reviewer-prompt.md +0 -0
  321. /package/{skills/conventions/references → docs/help}/lifecycle-and-savepoints.md +0 -0
  322. /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) },
@@ -94,78 +96,68 @@ module Arm
94
96
  }
95
97
  end
96
98
 
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
107
- end
108
-
109
- def write_pointer(session, value, home:)
99
+ # Owner rule 2026-08-31: has this session already started a conversation?
100
+ # A conversation session's tmp directory is made at boot under the global
101
+ # store's `.tmp/`; a dispatched or headless session (a derived key) never
102
+ # gets one. Reads only; rescues to false (fail open: a broken store must
103
+ # never block a legitimate delivery).
104
+ def started_session?(session, home:)
110
105
  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
106
+ Dir.exist?(SessionLedger.session_tmp_dir(store, SessionLedger.short_session_id(nil, session)))
107
+ rescue StandardError
108
+ false
116
109
  end
117
110
 
118
111
  # --- arm ---------------------------------------------------------------------
119
112
 
120
113
  # 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.
114
+ # mode and the provenance the caller knows) and provision the code worktree
115
+ # (fail-open for a global-only or non-git intent). Returns
116
+ # `{status:, lock:, worktree:, session:}`. A held, stale, excluded, or
117
+ # corrupt lock returns that status with the lock data read and touches
118
+ # nothing.
126
119
  def arm(intent_dir:, session:, mode: "auto", home: Dir.home, harness: nil,
127
120
  agent: nil, model: nil, thread: nil, now: Time.now, runner: Worktree::ShellRunner.new,
128
- host: Socket.gethostname)
121
+ host: Socket.gethostname, allow_inline: false)
129
122
  raise ArgumentError, "mode must be auto or guided" unless %w[auto guided].include?(mode.to_s)
130
123
  dir = File.expand_path(intent_dir)
131
124
  key = resolve_session(session, store: store_for(dir), intent_id: intent_id_for(dir))
132
125
  h = home_for(dir, home: home)
133
126
 
127
+ # Owner rule 2026-08-31: the main session never delivers an intent inline.
128
+ # A conversation session (its tmp directory already exists under the
129
+ # global store) arming an intent it does not already hold the lock for is
130
+ # inline delivery and is refused BEFORE any lock is taken. Re-arming the
131
+ # intent it already holds stays idempotent. A dispatched or headless
132
+ # session has no tmp directory and arms freely. --allow-inline is the
133
+ # explicit owner override.
134
+ if !allow_inline && started_session?(key, home: h) && !Lock.holds?(dir, session: key)
135
+ return { status: :inline_refused, lock: nil, worktree: nil, session: key }
136
+ end
137
+
134
138
  status, lock = Lock.acquire(dir, session: key, host: host, now: now,
135
139
  harness: harness, agent: agent, model: model,
136
140
  thread: thread, run_mode: mode.to_s)
137
141
  unless %i[acquired owned].include?(status)
138
- return { status: status, lock: lock, worktree: nil, session: key, pointer: nil }
142
+ return { status: status, lock: lock, worktree: nil, session: key }
139
143
  end
140
144
 
141
- data = bridge_hash(intent_dir: dir, home: h, with_worktree: false)
145
+ data = delivery(intent_dir: dir, home: h, with_worktree: false)
142
146
  begin
143
147
  Worktree.provision(data, home: h, runner: runner)
144
148
  rescue StandardError => e
145
149
  warn "plastic: worktree provision raised, continuing unprovisioned: #{e.message}"
146
150
  end
147
151
 
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
152
  { status: status, lock: lock, worktree: worktree_block(intent_dir: dir, home: h),
160
- session: key, pointer: pointer }
153
+ session: key }
161
154
  end
162
155
 
163
156
  # --- disarm ------------------------------------------------------------------
164
157
 
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).
158
+ # Give the intent back: remove the worktree (when `remove`) and release the
159
+ # lock as its recorded owner (falling back to `session`). Returns the lock
160
+ # release status (:released, :none, :not_owner, or :raised).
169
161
  def disarm(intent_dir:, session:, home: Dir.home, runner: Worktree::ShellRunner.new,
170
162
  remove: true, now: Time.now)
171
163
  dir = File.expand_path(intent_dir)
@@ -173,39 +165,24 @@ module Arm
173
165
  key = resolve_session(session, store: store_for(dir), intent_id: intent_id_for(dir))
174
166
 
175
167
  begin
176
- Worktree.release(bridge_hash(intent_dir: dir, home: h), home: h, runner: runner, remove: remove)
168
+ Worktree.release(delivery(intent_dir: dir, home: h), home: h, runner: runner, remove: remove)
177
169
  rescue StandardError => e
178
170
  warn "plastic: worktree release raised, continuing: #{e.message}"
179
171
  end
180
172
 
181
173
  lock = Lock.read(dir)
182
174
  owner = lock && !blank?(lock["owner_session"]) ? lock["owner_session"] : key
183
- release_status = begin
175
+ begin
184
176
  Lock.release(dir, session: owner)
185
177
  rescue StandardError => e
186
178
  warn "plastic: delivery lock release raised for #{dir}, continuing: #{e.message}"
187
179
  :raised
188
180
  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
181
  end
205
182
 
206
183
  # --- repair ------------------------------------------------------------------
207
184
 
208
- # One idempotent repair (the lock half of the bridge's repair, removed in 2.0):
185
+ # One idempotent repair (the lock half of the repair path):
209
186
  # remove a corrupt lock, back off from a fresh foreign lock (`held`), report
210
187
  # a stale foreign lock (`stale`) for the explicit reclaim verb, keep and
211
188
  # enrich an own lock, heartbeat a delegated one, acquire when none, and
@@ -259,7 +236,7 @@ module Arm
259
236
  end
260
237
 
261
238
  begin
262
- Worktree.provision(bridge_hash(intent_dir: dir, home: h, with_worktree: false), home: h, runner: runner)
239
+ Worktree.provision(delivery(intent_dir: dir, home: h, with_worktree: false), home: h, runner: runner)
263
240
  rescue StandardError => e
264
241
  warn "plastic: worktree provision raised during repair, continuing unprovisioned: #{e.message}"
265
242
  end
@@ -268,16 +245,4 @@ module Arm
268
245
 
269
246
  { "status" => "repaired", "actions" => actions, "lock" => lock_data, "session" => key }
270
247
  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
248
  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
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "intent_command"
4
+
5
+ # `plastic auto brief` - an intent's spawn preamble (`spawn-preamble`, run
6
+ # through Legacy). `--role advisor` also prints the three shapes an advisor
7
+ # request takes, so a dispatching lead never has to look them up.
8
+ module Plastic
9
+ class CLI
10
+ module Commands
11
+ class AutoBrief < IntentCommand
12
+ USAGE_LINE = "plastic auto brief ID [--role ROLE] [--json]"
13
+
14
+ SCRIPT = "spawn-preamble"
15
+ AFTER = "plastic auto report ID"
16
+ BECAUSE = "the report closes the turn the preamble opened"
17
+
18
+ ADVISOR_SHAPES =
19
+ "State the shape you need: a verdict plus the biggest risk for one bounded " \
20
+ "decision; a stepped plan plus a risk map for a plan or plan review; rival " \
21
+ "approaches and kill criteria for architecture, one-way doors, or deadlocks."
22
+
23
+ def call
24
+ super
25
+ @output.row("advisor request shapes", ADVISOR_SHAPES) if role == "advisor"
26
+ end
27
+
28
+ private
29
+
30
+ def role
31
+ options[:role]
32
+ end
33
+
34
+ def switches(parser)
35
+ parser.on("--role ROLE") { |v| @options[:role] = v }
36
+ end
37
+
38
+ def script_arguments
39
+ role ? [intent_dir, "--role", role] : [intent_dir]
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end