@zalom/plastic 2.0.0-alpha.8 → 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 +23 -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 +62 -1
  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 +238 -22
  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 +19 -5
  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 +30 -16
  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 +532 -71
  152. package/scripts/lib/intent_screen.rb +9 -6
  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 +377 -173
  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 +1460 -74
  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 +555 -0
  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 +192 -30
  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 +15 -2
  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 -295
  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 -173
  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
@@ -37,7 +37,7 @@
37
37
  # 2. Move the intent's INDEX.md line from `## Active` into `## Completed`
38
38
  # (delivered) or `## Abandoned` (abandoned), dated today, appending
39
39
  # --index-note (if given). Accepts a real em dash OR a plain hyphen as
40
- # the id/title separator on READ (shared matcher, Bridge.index_entry_match);
40
+ # the id/title separator on READ (shared matcher, IndexEntry.match);
41
41
  # always EMITS the real em dash on write. An id that resolves to neither
42
42
  # `## Active` nor the terminal section is a loud failure (exit 1); an id
43
43
  # already correctly in the terminal section is a quiet, idempotent success.
@@ -46,10 +46,9 @@
46
46
  # 5. Disarm (intent 188, D2/D16; intent 307): check the code worktree (derived
47
47
  # from projects.yml and the intent id, Arm.worktree_block) for uncommitted
48
48
  # changes first (exit 5 if dirty, unless --discard-worktree-changes); call the
49
- # disarm seam (default Arm.disarm: worktree released, lock cleared, the session
50
- # pointer back on the day ledger); verify the durable lock file is actually gone
51
- # afterward, and release it directly when a foreign-keyed disarm left it (the
52
- # /tmp bridge this step once read was removed in 2.0, intent 307).
49
+ # disarm seam (default Arm.disarm: worktree released, lock cleared); verify
50
+ # the durable lock file is actually gone afterward, and release it directly
51
+ # when a foreign-keyed disarm left it.
53
52
  #
54
53
  # Exit codes:
55
54
  # 0 ok - the intent is closed and no delivery.lock remains.
@@ -57,7 +56,7 @@
57
56
  # nor the terminal section (D11). Deliberately double duty; see plan.md.
58
57
  # 2 retired in 2.0 (intent 308): the outcome.md guard reports and proceeds.
59
58
  # 3 steps 1-4 already committed, but the delivery lock genuinely could not
60
- # be cleared (not a bridge cache miss; see step 5 above).
59
+ # be cleared (see step 5 above).
61
60
  # 4 pre-flight refusal: a FRESH foreign lock is held by a live,
62
61
  # non-delegate session; a FRESH lock that will not parse (corrupt); or a
63
62
  # lock file with no resolvable session identity at all. Authors nothing.
@@ -66,18 +65,23 @@
66
65
  # refused before removal; pass --discard-worktree-changes to override
67
66
  # deliberately.
68
67
  # 6 retired in 2.0 (intent 308): the structure check reports and proceeds.
68
+ # 7 hollow delivered report refused (317a D7) - intent 308's one deliberate
69
+ # exception to report-and-proceed; --allow-hollow-report overrides.
69
70
 
71
+ require_relative "lib/store_layout"
70
72
  require "fileutils"
71
73
  require "date"
72
74
  require "open3"
73
75
  require "pathname"
74
- require_relative "lib/bridge"
75
76
  require_relative "lib/arm"
77
+ require_relative "lib/index_entry"
76
78
  require_relative "lib/savepoint"
77
79
  require_relative "lib/lock"
78
80
  require_relative "lib/intent_validator"
79
81
  require_relative "lib/outcome_guard"
82
+ require_relative "lib/report_screen"
80
83
  require_relative "lib/backfill_intent"
84
+ require_relative "lib/outcome_report"
81
85
 
82
86
  DISPOSITIONS = %w[delivered abandoned].freeze
83
87
 
@@ -85,7 +89,7 @@ DISPOSITIONS = %w[delivered abandoned].freeze
85
89
  # dash (U+2014), the existing INDEX.md convention (store files are exempt from
86
90
  # the no-em-dash shipped-file rule). Built from the codepoint, not a literal
87
91
  # byte in this source file, so the script's own source stays em-dash free.
88
- # READING accepts a plain hyphen too, via the shared Bridge.index_entry_match
92
+ # READING accepts a plain hyphen too, via the shared IndexEntry.match
89
93
  # (intent 188, D9/D12): see move_index_to_terminal below.
90
94
  EM_DASH = "\u2014"
91
95
 
@@ -95,10 +99,15 @@ class UnresolvedIndexEntry < StandardError; end
95
99
 
96
100
  # --- Explicit flag parsing (no eval, no global injection) -------------------
97
101
 
102
+ def blank?(value)
103
+ value.nil? || value.to_s.strip.empty?
104
+ end
105
+
98
106
  def parse_args(argv)
99
107
  opts = {
100
108
  store: nil, id: nil, disposition: nil, index: nil,
101
109
  outcome_summary: nil, index_note: nil, no_commit: false, dry_run: false,
110
+ allow_hollow_report: false,
102
111
  session: nil, discard_worktree_changes: false,
103
112
  }
104
113
  i = 0
@@ -115,6 +124,7 @@ def parse_args(argv)
115
124
  when "--discard-worktree-changes" then opts[:discard_worktree_changes] = true
116
125
  when "--no-commit" then opts[:no_commit] = true
117
126
  when "--dry-run" then opts[:dry_run] = true
127
+ when "--allow-hollow-report" then opts[:allow_hollow_report] = true
118
128
  else
119
129
  usage_abort("unknown argument #{arg.inspect}")
120
130
  end
@@ -161,14 +171,65 @@ end
161
171
  # "projects" (the StoreDiscovery convention, scripts/lib/store_discovery.rb), never by
162
172
  # hardcoding ".plastic" as a name.
163
173
  def resolve_plastic_home_and_scope(store)
164
- parent = File.dirname(store)
165
- grandparent = File.dirname(parent)
166
- if File.basename(grandparent) == "projects"
167
- slug = File.basename(parent)
168
- [File.dirname(grandparent), "project:#{slug}"]
174
+ Plastic::StoreLayout.home_and_scope(store)
175
+ end
176
+
177
+ # --- outcome generation, before the backfill (intent 339, G6, spec D10) -----
178
+ #
179
+ # Generates outcome.md from graph.md, nodes/, and the ledger, BEFORE the
180
+ # backfill step runs, and only into a file that is missing or still the
181
+ # scaffold placeholder (`Savepoint.stage_file_present?` false) - a real,
182
+ # hand-written outcome.md is never touched (row 7.2). Only when a graph.md
183
+ # exists at all (row 7.3): every intent without one closes exactly as it did
184
+ # before this intent. The model is parsed AFTER the `stage_file_present?`
185
+ # guard, not before: a real hand-written outcome.md returns at that guard
186
+ # regardless of graph.md's own health, so a close that was never going to
187
+ # generate anything must never print a parse warning about it. Refuses a
188
+ # report model that reports itself broken (row v1f.2, B2): a malformed
189
+ # `## Graph` fails `model[:ok]` and the close falls straight through to the
190
+ # backfill, rather than adopting parse damage as delivery fact - agreeing
191
+ # with the shipped `outcome-report` CLI, which exits 3 on the same
192
+ # directory. The generated text is then checked against
193
+ # BOTH close gates - `OutcomeGuard` and the hollow-report gate below - before
194
+ # being adopted (row 7.4); when either would refuse it, the write is
195
+ # reverted and the close falls through to intent 308's unchanged backfill
196
+ # (row 7.5), so no close gains an exit 7 it did not have before this intent.
197
+ # Fails open on any exception (row 7.6), the same report-and-proceed shape
198
+ # every other close step in this file uses. `generator:` is the injectable
199
+ # seam (mirrors run_structure_check's `gate:`).
200
+ def run_generate_outcome(intent_dir, disposition:, generator: OutcomeReport.method(:write))
201
+ graph_path = File.join(intent_dir, "graph.md")
202
+ return unless File.exist?(graph_path)
203
+
204
+ outcome_path = File.join(intent_dir, "outcome.md")
205
+ return if Savepoint.stage_file_present?(outcome_path)
206
+
207
+ report_model = OutcomeReport.model(intent_dir)
208
+ unless report_model[:ok]
209
+ warn "end-intent: graph.md failed to parse (#{report_model[:errors].join('; ')}); falling back to the backfill"
210
+ return
211
+ end
212
+
213
+ original_exists = File.exist?(outcome_path)
214
+ original_content = original_exists ? File.read(outcome_path) : nil
215
+
216
+ generator.call(intent_dir, disposition: disposition)
217
+
218
+ guard_reason = OutcomeGuard.reason(intent_dir, disposition)
219
+ hollow = hollow_report_reason(intent_dir, disposition)
220
+ if guard_reason || hollow
221
+ if original_exists
222
+ File.write(outcome_path, original_content)
223
+ elsif File.exist?(outcome_path)
224
+ File.delete(outcome_path)
225
+ end
226
+ warn "end-intent: generated outcome.md would be refused (#{[guard_reason, hollow].compact.join('; ')}); " \
227
+ "falling back to the backfill"
169
228
  else
170
- [parent, "global"]
229
+ warn "end-intent: outcome.md generated from graph.md and the ledger"
171
230
  end
231
+ rescue StandardError => e
232
+ warn "end-intent: outcome generation crashed (#{e.message}); proceeding without it"
172
233
  end
173
234
 
174
235
  # Backfill (intent 308): write the judgment documents that are missing or still the
@@ -215,7 +276,137 @@ end
215
276
  # Replace the body of the intent file's `## Outcome` section with `summary`,
216
277
  # leaving every other section and the frontmatter untouched. No-op (returns
217
278
  # content unchanged) when summary is blank or the section is not found.
279
+ # 317a S9 (D7): the self-render gate. Renders the delivered screen's sources on
280
+ # the intent's own record and names what would come out hollow. This is intent
281
+ # 308's ONE deliberate exception to report-and-proceed: a hollow DELIVERED
282
+ # close is refused with exit 7 (--allow-hollow-report overrides), because 317
283
+ # closed exactly this way and the owner read a screen full of "not recorded".
284
+ # Abandoned closes and machine-backfilled outcomes (the backfill marker) are
285
+ # exempt: the reader cannot expect labels the writer never had.
286
+ #
287
+ # Intent 334 (G1, review A3; post-execution review, blocking 2): the legacy
288
+ # "S3" label grammar applies only to actions/*.md headings, and the node id
289
+ # grammar (kind prefix n/v/d/r plus digits, D1r) applies only to nodes/*.md
290
+ # headings - never both over both directories. This repo's own house words
291
+ # for plan review and post-execution review are "v1" and "v2"; widening the
292
+ # node-id alternative onto actions/ headings turned that prose into a
293
+ # manufactured label and refused legitimate legacy closes at exit 7.
294
+ ACTION_ID_LABEL_RE = /\AS\d+\z/.freeze
295
+ NODE_ID_LABEL_RE = /\A[nvdr][1-9]\d*\z/.freeze
296
+
297
+ # The id-shaped tokens found in `paths`' headings, matched against
298
+ # `id_label_re` only - the caller picks the grammar for the directory.
299
+ def heading_id_labels(paths, id_label_re)
300
+ paths.flat_map do |path|
301
+ ReportScreen.split_by_headings(File.read(path)).flat_map do |heading, _body|
302
+ heading.to_s.sub(/\A#+\s*/, "").split(/[^A-Za-z0-9]+/).select { |tok| tok.match?(id_label_re) }
303
+ end
304
+ end
305
+ end
306
+
307
+ # 339 S9 (D18): a node id counts as a label in an actions/*.md heading too,
308
+ # but only when it stands alone as one of the heading's " - "-delimited
309
+ # segments - this repo's own convention, "### S1 - n1 - Title" (this action
310
+ # file's own headings, post-merge) or the bare "### n1 - Title". Never as a
311
+ # free word inside ordinary prose: the comment above ACTION_ID_LABEL_RE
312
+ # records exactly why intent 334 restricted the node-id grammar to
313
+ # nodes/*.md headings in the first place, and that reasoning does not change
314
+ # here - a word-scan widened onto actions/ headings would let this repo's own
315
+ # house words ("v1", "v2" for plan review and post-execution review)
316
+ # manufacture a label out of ordinary prose and refuse a legitimate legacy
317
+ # close. Segment position is what a word-scan cannot see: "v2" embedded
318
+ # mid-sentence is never its own " - "-delimited segment, but a heading built
319
+ # to this convention always puts the node id in one.
320
+ def heading_node_id_segments(paths, id_label_re)
321
+ paths.flat_map do |path|
322
+ ReportScreen.split_by_headings(File.read(path)).flat_map do |heading, _body|
323
+ heading.to_s.sub(/\A#+\s*/, "").split(/\s+-\s+/).map(&:strip).select { |seg| seg.match?(id_label_re) }
324
+ end
325
+ end
326
+ end
327
+
328
+ def hollow_report_reason(intent_dir, disposition)
329
+ return nil unless disposition == "delivered"
330
+
331
+ outcome = File.join(intent_dir, "outcome.md")
332
+ return nil unless File.exist?(outcome)
333
+ text = File.read(outcome)
334
+ return nil if text.include?("<!-- backfilled from the record by end-intent on ")
335
+
336
+ # The gate's evidence bar (A7/B7): it judges only records that CLAIM a
337
+ # labeled convention - at least one actions/*.md heading carrying an
338
+ # S-label or a node-id segment, or at least one nodes/*.md heading carrying
339
+ # a node id ("n1", "v2", "d3", "r4"). A terse legacy close with no labeled
340
+ # matrix stays report-and-proceed (intent 334, G1, D10r).
341
+ #
342
+ # Every matching token per heading is kept, not just the first (review A3):
343
+ # a legacy actions/ heading can carry BOTH an S-label and prose that reads
344
+ # like a node id ("### The v2 rewrite (S3)"). Restricting the word-scan
345
+ # node-id grammar to nodes/*.md headings (post-execution review, blocking
346
+ # 2) means "v2" in that heading is never even tested against it, so the
347
+ # heading's real "S3" label is never at risk.
348
+ #
349
+ # 339 S9 (D18): a graph-era action file headed by node id alone ("### n1 -
350
+ # Title", no S-label) used to yield an empty label set here and the gate
351
+ # returned before judging anything - every such record closed unjudged by
352
+ # silent exemption. `heading_node_id_segments` widens the actions/*.md side
353
+ # to see that shape without reopening the word-scan hole above: it matches
354
+ # a node id only when the heading's own " - "-delimited segments carry one,
355
+ # never a free word in prose. The node_paths call stays exactly as it was;
356
+ # nodes/*.md headings already resolve node ids correctly (test_node_id_
357
+ # labels_are_recognized) and their heading grammar is controlled, so a
358
+ # word-scan there carries none of the prose risk.
359
+ action_paths = Dir.glob(File.join(intent_dir, "actions", "*.md")).sort
360
+ node_paths = Dir.glob(File.join(intent_dir, "nodes", "*.md")).sort
361
+ heading_labels = (heading_id_labels(action_paths, ACTION_ID_LABEL_RE) +
362
+ heading_node_id_segments(action_paths, NODE_ID_LABEL_RE) +
363
+ heading_id_labels(node_paths, NODE_ID_LABEL_RE)).uniq
364
+ return nil if heading_labels.empty?
365
+
366
+ rows = ReportScreen.delivered_rows(intent_dir)
367
+ return "outcome.md ## Delivered renders no rows although a labeled action matrix exists" if rows.empty?
368
+
369
+ needs = ReportScreen.section_of(text, "## Needs you").gsub(/<!--.*?-->/m, "").strip
370
+ if !needs.empty? && needs != "None" && ReportScreen.needs_you_rows(intent_dir).empty?
371
+ return "## Needs you has content that would render as None"
372
+ end
373
+
374
+ labels = rows.map { |r| r[:label] }
375
+ if (labels & heading_labels).empty?
376
+ return "delivered row labels (#{labels.first(3).join(', ')}...) match no action-file heading (#{heading_labels.first(3).join(', ')}...)"
377
+ end
378
+ if rows.all? { |r| ReportScreen.proven_by(intent_dir, r[:label]) == ReportScreen::NOT_RECORDED }
379
+ return "every Proven-by cell renders not recorded although labeled action headings exist"
380
+ end
381
+
382
+ nil
383
+ rescue StandardError => e
384
+ warn "end-intent: hollow-report gate crashed (#{e.message}); proceeding without it"
385
+ nil
386
+ end
387
+
388
+ # 317a S7 (D5, A8): stamp the live lock's run_mode into outcome.md frontmatter
389
+ # INSIDE the close step - after the gate, before the INDEX move and the store
390
+ # commit - so the stamped file rides the close commit and mode survives disarm.
391
+ def stamp_outcome_mode(intent_dir)
392
+ data = Lock.read(intent_dir)
393
+ run_mode = data && data["run_mode"].to_s
394
+ return if run_mode.nil? || run_mode.empty?
395
+
396
+ path = File.join(intent_dir, "outcome.md")
397
+ return unless File.exist?(path)
398
+ text = File.read(path)
399
+ return unless text.start_with?("---")
400
+ head, body = text[3..].split("---", 2)
401
+ return if body.nil? || head.match?(/^mode:/)
402
+
403
+ File.write(path, "---#{head}mode: #{run_mode}\n---#{body}")
404
+ rescue StandardError => e
405
+ warn "end-intent: mode stamp failed (#{e.message}); proceeding without it"
406
+ end
407
+
218
408
  def stamp_outcome_summary(content, summary)
409
+
219
410
  return content if summary.nil? || summary.to_s.strip.empty?
220
411
 
221
412
  lines = content.lines
@@ -244,10 +435,10 @@ def section_stop(lines, heading_idx)
244
435
  stop
245
436
  end
246
437
 
247
- # Shared matcher (Bridge.index_entry_match, intent 188 D12): accepts a real em
438
+ # Shared matcher (IndexEntry.match, intent 188 D12): accepts a real em
248
439
  # dash OR a plain hyphen as the id/title separator on READ.
249
440
  def active_line_id(line)
250
- m = Bridge.index_entry_match(line)
441
+ m = IndexEntry.match(line)
251
442
  m && m[1]
252
443
  end
253
444
 
@@ -326,7 +517,7 @@ def move_index_to_terminal(content, id, disposition, today:, index_note: nil)
326
517
  raise UnresolvedIndexEntry, "INDEX.md has no #{target_heading} heading; cannot complete the move"
327
518
  end
328
519
 
329
- m = Bridge.index_entry_match(lines[entry_idx])
520
+ m = IndexEntry.match(lines[entry_idx])
330
521
  title, link = m[2], m[3]
331
522
  note_suffix = (index_note && !index_note.to_s.strip.empty?) ? " #{index_note.to_s.strip}" : ""
332
523
  new_entry = "- [#{id} #{EM_DASH} #{title}](#{link}) #{EM_DASH} #{today}#{note_suffix}\n"
@@ -415,13 +606,13 @@ end
415
606
  # exclusion with a raw backtrace. nil is the only safe "could not resolve"
416
607
  # signal; main refuses explicitly on it rather than ever reaching takeover.
417
608
  def resolve_end_session(explicit, intent_dir)
418
- return explicit.to_s.strip unless Bridge.blank?(explicit)
609
+ return explicit.to_s.strip unless blank?(explicit)
419
610
  env = ENV["CLAUDE_CODE_SESSION_ID"]
420
- return env.to_s.strip unless Bridge.blank?(env)
611
+ return env.to_s.strip unless blank?(env)
421
612
  lock = Lock.read(intent_dir)
422
613
  return nil unless lock
423
614
  owner = lock["owner_session"].to_s
424
- Bridge.blank?(owner) ? nil : owner
615
+ blank?(owner) ? nil : owner
425
616
  end
426
617
 
427
618
  # Read-only pre-flight verdict (D4). Returns [:proceed, lock_or_nil],
@@ -469,7 +660,7 @@ end
469
660
  def run_disarm(intent_dir, id, session, discard_worktree_changes:,
470
661
  worktree_reader: ->(dir) { Arm.worktree_block(intent_dir: dir) },
471
662
  disarm: ->(sess, dir) { Arm.disarm(intent_dir: dir, session: sess) })
472
- return :ok if Bridge.blank?(session)
663
+ return :ok if blank?(session)
473
664
 
474
665
  block = worktree_reader.call(intent_dir)
475
666
  worktree_code = block.is_a?(Hash) ? block["code"] : nil
@@ -594,7 +785,7 @@ def main(argv)
594
785
  # Lock.takeover (blank session reaches Lock.acquire's blank? guard AFTER the
595
786
  # existing lock file was already deleted) or silently mis-arbitrate. Refuse
596
787
  # rather than guess; nothing has been written yet.
597
- if Bridge.blank?(session) && File.exist?(Lock.path(intent_dir))
788
+ if blank?(session) && File.exist?(Lock.path(intent_dir))
598
789
  warn "end-intent: a delivery lock exists at #{Lock.path(intent_dir)} but no session " \
599
790
  "identity could be resolved (--session, CLAUDE_CODE_SESSION_ID, and the lock's " \
600
791
  "own recorded owner are all blank); refusing rather than guessing. Pass " \
@@ -645,6 +836,11 @@ def main(argv)
645
836
  end
646
837
  end
647
838
 
839
+ # 0a. Generate outcome.md from graph.md and the ledger, when there is one
840
+ # (intent 339, G6, spec D10) - before the backfill, so a real graph never
841
+ # falls through to the terse backfill it exists to replace.
842
+ run_generate_outcome(intent_dir, disposition: disposition)
843
+
648
844
  # 1. Backfill from the record (intent 308), then the structure self-check and the
649
845
  # outcome guard, both report-only since 308.
650
846
  run_backfill(intent_dir, store: store, id: id, disposition: disposition,
@@ -654,6 +850,26 @@ def main(argv)
654
850
  reason = OutcomeGuard.reason(intent_dir, disposition)
655
851
  warn "end-intent: outcome.md: #{reason} (proceeding; doctor --intent #{id} reports it)" if reason
656
852
 
853
+ # 1a2. The self-render gate (317a S9, D7): refuse a hollow delivered close
854
+ # BEFORE any terminal mutation - INDEX, savepoint, and store stay untouched
855
+ # on exit 7 (A9).
856
+ hollow = hollow_report_reason(intent_dir, disposition)
857
+ if hollow
858
+ if opts[:allow_hollow_report]
859
+ warn "end-intent: hollow delivered report overridden (--allow-hollow-report): #{hollow}"
860
+ else
861
+ warn "end-intent: refusing a hollow delivered close: #{hollow}"
862
+ warn "end-intent: write outcome.md's ## Delivered as the labeled | Row | What | table " \
863
+ "matching the action-file headings (templates/outcome.md shows the shape), " \
864
+ "or pass --allow-hollow-report to close anyway"
865
+ exit 7
866
+ end
867
+ end
868
+
869
+ # 1a3. Mode stamp (317a S7, D5): run_mode into outcome frontmatter while the
870
+ # lock still exists, so the stamped file rides the close commit.
871
+ stamp_outcome_mode(intent_dir)
872
+
657
873
  # 1b. Intent-file `## Outcome` summary stamp (no-op unless given).
658
874
  if opts[:outcome_summary]
659
875
  intent_file = Savepoint.intent_file(intent_dir)
@@ -21,15 +21,15 @@
21
21
  # (--session, else CLAUDE_CODE_SESSION_ID, else the existing delivery.lock's own
22
22
  # recorded owner_session when non-blank, else nil; the lock-owner fallback IS
23
23
  # used here, matching end-intent's teardown resolution order).
24
- # Derive the worktree block from projects.yml and the intent id (Arm.worktree_block;
25
- # the /tmp bridge this once read was removed in 2.0, intent 307). No code worktree on
24
+ # Derive the worktree block from projects.yml and the intent id (Arm.worktree_block).
25
+ # No code worktree on
26
26
  # disk means nothing was provisioned (exit 0).
27
27
  # 1. The dirty-worktree guard (delivered only): git status --porcelain on the code
28
28
  # worktree. Not clean, or the check itself fails, fails CLOSED (exit 3), mirroring
29
29
  # end-intent's dirty-worktree handling. abandoned skips this guard entirely: the
30
30
  # branch survives and can be reclaimed, so a dirty worktree is not a reason to
31
31
  # refuse.
32
- # 2. Finish: Worktree.finish(bridge_data, home:, runner:, merge: disposition ==
32
+ # 2. Finish: Worktree.finish(delivery, home:, runner:, merge: disposition ==
33
33
  # "delivered"). Worktree.finish is fail-open and never raises, so failure is
34
34
  # detected by checking, for a delivered disposition only, whether the worktree
35
35
  # directory still exists on disk afterward (release removes it on success).
@@ -38,8 +38,8 @@
38
38
  # one concern per script (spec D8).
39
39
  #
40
40
  # Exit codes:
41
- # 0 finished (merged or removed per disposition), or nothing to finish (no bridge
42
- # resolved, or no code worktree recorded on the bridge)
41
+ # 0 finished (merged or removed per disposition), or nothing to finish (no delivery
42
+ # resolved, or no code worktree recorded on it)
43
43
  # 1 usage or path-resolution failure
44
44
  # 2 retired in 2.0 (intent 302): was the order precondition; never emitted, never reused
45
45
  # 3 the worktree is not clean, or the merge/removal did not complete (delivered only)
@@ -14,6 +14,7 @@
14
14
  # and on any filing error (reported on stderr; the next boot tries again), 2 on
15
15
  # a usage error.
16
16
 
17
+ require_relative "lib/store_layout"
17
18
  require_relative "lib/session_ledger"
18
19
  require_relative "lib/session_backfill"
19
20
 
@@ -48,7 +49,7 @@ if opts[:carry_to] && !SessionLedger.valid_day_id?(opts[:carry_to])
48
49
  usage_abort("--carry-to must be a real calendar date, YYYYMMDD")
49
50
  end
50
51
 
51
- store = opts[:store] || File.join(File.expand_path(ENV.fetch("PLASTIC_HOME", "~/.plastic")), "store")
52
+ store = opts[:store] || Plastic::StoreLayout.global_store(File.expand_path(ENV.fetch("PLASTIC_HOME", "~/.plastic")))
52
53
  templates = opts[:templates] || File.expand_path("../templates", __dir__)
53
54
 
54
55
  begin