@zalom/plastic 1.14.1 → 2.0.0-alpha.10

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 (213) hide show
  1. package/PLASTIC.md +101 -294
  2. package/README.md +5 -6
  3. package/agents/plastic-advisor.md +18 -17
  4. package/agents/plastic-enforcer.md +57 -69
  5. package/agents/plastic-executor.md +34 -20
  6. package/agents/plastic-faux-advisor.md +18 -18
  7. package/bin/lib/context_budget.rb +453 -0
  8. package/bin/plastic-bench +78 -0
  9. package/config_asks.yml +38 -0
  10. package/hooks/capture +4 -0
  11. package/hooks/close +5 -0
  12. package/hooks/hooks.json +14 -42
  13. package/hooks/message-display +81 -0
  14. package/hooks/record +4 -0
  15. package/hooks/savepoint +5 -5
  16. package/package.json +3 -2
  17. package/scripts/agent-report +15 -10
  18. package/scripts/append-ledger +208 -0
  19. package/scripts/codex-hook +78 -90
  20. package/scripts/dashboard.rb +43 -14
  21. package/scripts/day-summary +53 -0
  22. package/scripts/doctor.rb +269 -43
  23. package/scripts/end-intent +170 -76
  24. package/scripts/exec-worktree +11 -20
  25. package/scripts/file-session-intent +61 -0
  26. package/scripts/hook-capture +255 -0
  27. package/scripts/hook-close +36 -0
  28. package/scripts/hook-message-display +74 -0
  29. package/scripts/hook-record +157 -0
  30. package/scripts/hook-savepoint +45 -0
  31. package/scripts/hook-session-start +128 -12
  32. package/scripts/install.rb +8 -6
  33. package/scripts/intent-screen +77 -0
  34. package/scripts/lib/agent_models.rb +1 -7
  35. package/scripts/lib/arm.rb +308 -0
  36. package/scripts/lib/backfill_intent.rb +316 -0
  37. package/scripts/lib/bridge.rb +24 -1597
  38. package/scripts/lib/compact_instructions.rb +56 -0
  39. package/scripts/lib/day_summary.rb +211 -0
  40. package/scripts/lib/doctor_core.rb +66 -88
  41. package/scripts/lib/doctor_session_ledger.rb +158 -0
  42. package/scripts/lib/exec_worktree.rb +14 -60
  43. package/scripts/lib/handoff.rb +184 -0
  44. package/scripts/lib/harness_text.rb +5 -4
  45. package/scripts/lib/hook_registry.rb +57 -106
  46. package/scripts/lib/insights.rb +3 -4
  47. package/scripts/lib/installer_core.rb +136 -41
  48. package/scripts/lib/intent_screen.rb +309 -0
  49. package/scripts/lib/intent_screen_ansi.rb +262 -0
  50. package/scripts/lib/lock.rb +6 -26
  51. package/scripts/lib/message_display.rb +290 -0
  52. package/scripts/lib/outcome_guard.rb +7 -5
  53. package/scripts/lib/project_validator.rb +30 -1
  54. package/scripts/lib/report_screen.rb +648 -0
  55. package/scripts/lib/roadmap_queue.rb +3 -3
  56. package/scripts/lib/roadmap_savepoint.rb +1 -1
  57. package/scripts/lib/rule_catalog.rb +6 -3
  58. package/scripts/lib/savepoint.rb +334 -0
  59. package/scripts/lib/scaffold_intent.rb +13 -179
  60. package/scripts/lib/screen_paint.rb +276 -0
  61. package/scripts/lib/session_backfill.rb +184 -0
  62. package/scripts/lib/session_close.rb +104 -0
  63. package/scripts/lib/session_git.rb +500 -0
  64. package/scripts/lib/session_ledger.rb +621 -0
  65. package/scripts/lib/store_discovery.rb +1 -1
  66. package/scripts/lib/store_provisioning.rb +1 -1
  67. package/scripts/maintenance-run +7 -7
  68. package/scripts/new-intent +63 -4
  69. package/scripts/plastic-lock +101 -41
  70. package/scripts/promote-session-item +150 -0
  71. package/scripts/read-config +3 -0
  72. package/scripts/rebuild-graph +1 -1
  73. package/scripts/report-screen +120 -0
  74. package/scripts/roadmap-next +2 -2
  75. package/scripts/rollback.rb +48 -3
  76. package/scripts/savepoint-note +67 -0
  77. package/scripts/scaffold-intent +37 -63
  78. package/scripts/session-commit +116 -0
  79. package/scripts/spawn-preamble +14 -7
  80. package/scripts/update.rb +7 -3
  81. package/scripts/write-handoff +60 -0
  82. package/skills/agent-advisor/SKILL.md +3 -3
  83. package/skills/agent-advisor/references/advisor-protocol.md +20 -20
  84. package/skills/auto/SKILL.md +223 -317
  85. package/skills/auto/evals/evals.json +10 -10
  86. package/skills/auto/references/agent-architecture.md +102 -90
  87. package/skills/auto/references/agent-report-contract.md +4 -21
  88. package/skills/auto/references/end-tail.md +47 -53
  89. package/skills/auto/references/human-report-contract.md +62 -56
  90. package/skills/conventions/SKILL.md +1 -3
  91. package/skills/conventions/references/lifecycle-and-savepoints.md +35 -1
  92. package/skills/conventions/references/locks-and-worktrees.md +62 -58
  93. package/skills/conventions/references/maintenance-and-revisions.md +12 -12
  94. package/skills/dashboard/SKILL.md +1 -1
  95. package/skills/direct/SKILL.md +66 -0
  96. package/skills/direct/references/request-signals.md +59 -0
  97. package/skills/doctor/SKILL.md +58 -11
  98. package/skills/doctor/report.md +3 -1
  99. package/skills/install/SKILL.md +11 -9
  100. package/skills/intent-continuing/SKILL.md +142 -91
  101. package/skills/intent-continuing/references/boarding-matrix.md +34 -0
  102. package/skills/intent-continuing/references/context-management.md +3 -3
  103. package/skills/intent-creating/SKILL.md +2 -2
  104. package/skills/intent-creating/references/lifecycle.md +1 -1
  105. package/skills/intent-ending/SKILL.md +34 -37
  106. package/skills/intent-ending/evals/evals.json +4 -4
  107. package/skills/intent-executing/SKILL.md +15 -46
  108. package/skills/intent-executing/plan-reviewer-prompt.md +39 -0
  109. package/skills/intent-speccing/SKILL.md +108 -51
  110. package/skills/intent-speccing/evals/evals.json +5 -5
  111. package/skills/intent-speccing/references/per-section-fill-rules.md +4 -4
  112. package/skills/intent-speccing/references/self-verify-checklist.md +1 -2
  113. package/skills/project-creating/SKILL.md +1 -1
  114. package/skills/releasing/SKILL.md +7 -8
  115. package/skills/tutorial/SKILL.md +2 -2
  116. package/skills/tutorial/references/track-1-guided.md +16 -18
  117. package/skills/tutorial/references/track-2-auto.md +12 -13
  118. package/skills/tutorial/references/track-3-projects-and-roadmaps.md +2 -2
  119. package/templates/agents.md +4 -7
  120. package/templates/config.yml +5 -0
  121. package/templates/intent-screen.md +17 -0
  122. package/templates/outcome.md +14 -1
  123. package/templates/project.yml +7 -0
  124. package/templates/report-state.md +11 -0
  125. package/templates/roadmap.md +1 -1
  126. package/templates/savepoint.md +2 -2
  127. package/templates/session-intent.md +29 -0
  128. package/agents/plastic-brainstorming.md +0 -48
  129. package/agents/plastic-future-intent-researcher.md +0 -35
  130. package/agents/plastic-intent-curator.md +0 -47
  131. package/agents/plastic-intent-discovery.md +0 -43
  132. package/agents/plastic-planner.md +0 -56
  133. package/agents/plastic-spec-specialist.md +0 -53
  134. package/hooks/auto-arm +0 -5
  135. package/hooks/bash-gate +0 -3
  136. package/hooks/continue +0 -31
  137. package/hooks/edit-gates +0 -3
  138. package/hooks/future-intent-check +0 -25
  139. package/hooks/gate-check +0 -12
  140. package/hooks/power-tools +0 -8
  141. package/scripts/hook-auto-arm +0 -51
  142. package/scripts/hook-bash-gate +0 -78
  143. package/scripts/hook-code-gate +0 -38
  144. package/scripts/hook-continue +0 -48
  145. package/scripts/hook-create-gate +0 -51
  146. package/scripts/hook-edit-gates +0 -58
  147. package/scripts/hook-future-intent-check +0 -90
  148. package/scripts/hook-gate-check +0 -169
  149. package/scripts/hook-links-gate +0 -54
  150. package/scripts/hook-lock-gate +0 -47
  151. package/scripts/hook-power-tools +0 -38
  152. package/scripts/hook-savepoint-pre +0 -32
  153. package/scripts/lib/codex_edit_gates.rb +0 -138
  154. package/scripts/lib/edit_gates.rb +0 -398
  155. package/scripts/lib/links_gate.rb +0 -140
  156. package/scripts/lib/qmd_hook.rb +0 -41
  157. package/scripts/lib/spec_header.rb +0 -83
  158. package/scripts/lib/start_intent.rb +0 -296
  159. package/scripts/start-intent +0 -89
  160. package/skills/_active-intent-gate.md +0 -26
  161. package/skills/auto/references/tiers.md +0 -100
  162. package/skills/continuing/SKILL.md +0 -34
  163. package/skills/continuing/evals/evals.json +0 -91
  164. package/skills/conventions/references/gates-and-enforcement.md +0 -53
  165. package/skills/conventions/references/tiers-and-dispatch.md +0 -135
  166. package/skills/doctor/references/gates-stuck-detection.md +0 -43
  167. package/skills/intent-brainstorming/SKILL.md +0 -118
  168. package/skills/intent-brainstorming/evals/evals.json +0 -67
  169. package/skills/intent-continuing/evals/evals.json +0 -145
  170. package/skills/intent-discovering/SKILL.md +0 -56
  171. package/skills/intent-grilling/SKILL.md +0 -108
  172. package/skills/intent-linking/SKILL.md +0 -128
  173. package/skills/intent-linking/evals/evals.json +0 -22
  174. package/skills/intent-linking/references/zettelkasten.md +0 -45
  175. package/skills/intent-locking/SKILL.md +0 -64
  176. package/skills/intent-planning/SKILL.md +0 -145
  177. package/skills/intent-planning/evals/evals.json +0 -81
  178. package/skills/intent-planning/references/plan-format.md +0 -130
  179. package/skills/intent-researching/SKILL.md +0 -123
  180. package/skills/intent-researching/evals/evals.json +0 -22
  181. package/skills/intent-savepoint/SKILL.md +0 -83
  182. package/skills/intent-savepoint/references/context-management.md +0 -32
  183. package/skills/intent-starting/SKILL.md +0 -151
  184. package/skills/intent-starting/evals/evals.json +0 -117
  185. package/skills/intent-starting/references/boarding-matrix.md +0 -35
  186. package/skills/project-continuing/SKILL.md +0 -119
  187. package/skills/project-continuing/evals/evals.json +0 -100
  188. package/skills/roadmap-continuing/SKILL.md +0 -89
  189. package/skills/roadmap-continuing/evals/evals.json +0 -82
  190. package/skills/skill-creating/SKILL.md +0 -75
  191. package/skills/skill-creating/evals/evals.json +0 -108
  192. package/skills/skill-creating/references/agents.md +0 -168
  193. package/skills/skill-creating/references/defaults-first.md +0 -23
  194. package/skills/skill-creating/references/evals.md +0 -41
  195. package/skills/skill-creating/references/hooks.md +0 -251
  196. package/skills/skill-creating/references/progressive-disclosure.md +0 -176
  197. package/skills/skill-creating/references/scripts.md +0 -166
  198. package/skills/skill-creating/references/skills.md +0 -169
  199. package/skills/skill-creating/scripts/scaffold.rb +0 -313
  200. package/skills/skill-evaluating/SKILL.md +0 -141
  201. package/skills/skill-evaluating/assets/eval-template.json +0 -12
  202. package/skills/skill-evaluating/evals/evals.json +0 -75
  203. package/skills/skill-evaluating/references/convention-checks.md +0 -76
  204. package/skills/skill-evaluating/references/eval-methodology.md +0 -154
  205. package/skills/store-curating/SKILL.md +0 -64
  206. package/skills/store-curating/evals/evals.json +0 -38
  207. package/skills/store-indexing/SKILL.md +0 -93
  208. package/skills/store-indexing/evals/evals.json +0 -22
  209. package/skills/store-indexing/references/zettelkasten-linking.md +0 -32
  210. package/skills/store-provisioning/SKILL.md +0 -55
  211. /package/skills/{project-continuing → intent-continuing}/references/board-fill.md +0 -0
  212. /package/skills/{roadmap-continuing → intent-continuing}/references/liveness-ranking.md +0 -0
  213. /package/skills/{intent-brainstorming → intent-speccing}/references/design-principles.md +0 -0
@@ -28,8 +28,12 @@
28
28
  # file that exists while no session identity resolves from ANY source
29
29
  # also refuses (exit 4) rather than ever reaching Lock.takeover with a
30
30
  # blank session, which would delete the lock file and then crash.
31
- # 1. Guard outcome.md (exit 2, authors nothing, on any failure), then stamp
32
- # the intent file's `## Outcome` section with --outcome-summary if given.
31
+ # 1. Backfill (intent 308): write spec.md, plan.md, actions/ACTION_1.md, and
32
+ # outcome.md from the record wherever a file is missing or still the
33
+ # placeholder (BackfillIntent; real content is never touched). Then run
34
+ # doctor's per-intent structure check and OutcomeGuard as a self-check
35
+ # that reports on stderr and proceeds. Then stamp the intent file's
36
+ # `## Outcome` section with --outcome-summary if given.
33
37
  # 2. Move the intent's INDEX.md line from `## Active` into `## Completed`
34
38
  # (delivered) or `## Abandoned` (abandoned), dated today, appending
35
39
  # --index-note (if given). Accepts a real em dash OR a plain hyphen as
@@ -37,23 +41,21 @@
37
41
  # always EMITS the real em dash on write. An id that resolves to neither
38
42
  # `## Active` nor the terminal section is a loud failure (exit 1); an id
39
43
  # already correctly in the terminal section is a quiet, idempotent success.
40
- # 3. Append the savepoint `Done` bookend (Bridge.append_terminal_savepoint).
44
+ # 3. Append the savepoint `Done` bookend (Savepoint.append_terminal_savepoint).
41
45
  # 4. Commit the store repo, unless --no-commit.
42
- # 5. Disarm (intent 188, D2/D16): check the code worktree (resolved from the
43
- # bridge) for uncommitted changes first (exit 5 if dirty, unless
44
- # --discard-worktree-changes); call the disarm seam (default
45
- # Bridge.disarm_auto); verify the durable lock file is actually gone
46
- # afterward. The /tmp bridge is only a cache (AGENTS.md): when no bridge
47
- # resolves at all (a wiped /tmp, a resumed job under a new session id),
48
- # the durable lock file is released directly instead, LOUDLY (a warning
49
- # that a worktree may be orphaned), rather than stranding a committed,
50
- # terminal intent still holding its lock.
46
+ # 5. Disarm (intent 188, D2/D16; intent 307): check the code worktree (derived
47
+ # from projects.yml and the intent id, Arm.worktree_block) for uncommitted
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).
51
53
  #
52
54
  # Exit codes:
53
55
  # 0 ok - the intent is closed and no delivery.lock remains.
54
56
  # 1 usage/resolution failure, OR an id that resolves to neither ## Active
55
57
  # nor the terminal section (D11). Deliberately double duty; see plan.md.
56
- # 2 the outcome.md guard refused; authors nothing.
58
+ # 2 retired in 2.0 (intent 308): the outcome.md guard reports and proceeds.
57
59
  # 3 steps 1-4 already committed, but the delivery lock genuinely could not
58
60
  # be cleared (not a bridge cache miss; see step 5 above).
59
61
  # 4 pre-flight refusal: a FRESH foreign lock is held by a live,
@@ -63,19 +65,22 @@
63
65
  # (git status itself failed: corrupt index, git missing, not a repo);
64
66
  # refused before removal; pass --discard-worktree-changes to override
65
67
  # deliberately.
66
- # 6 the doctor per-intent structure gate refused (checklist incomplete,
67
- # links out of projection, intent-file malformed, or a lifecycle artifact
68
- # missing/placeholder); named reasons on stderr; authors nothing
69
- # (intent 222).
68
+ # 6 retired in 2.0 (intent 308): the structure check reports and proceeds.
69
+ # 7 hollow delivered report refused (317a D7) - intent 308's one deliberate
70
+ # exception to report-and-proceed; --allow-hollow-report overrides.
70
71
 
71
72
  require "fileutils"
72
73
  require "date"
73
74
  require "open3"
74
75
  require "pathname"
75
76
  require_relative "lib/bridge"
77
+ require_relative "lib/arm"
78
+ require_relative "lib/savepoint"
76
79
  require_relative "lib/lock"
77
80
  require_relative "lib/intent_validator"
78
81
  require_relative "lib/outcome_guard"
82
+ require_relative "lib/report_screen"
83
+ require_relative "lib/backfill_intent"
79
84
 
80
85
  DISPOSITIONS = %w[delivered abandoned].freeze
81
86
 
@@ -97,6 +102,7 @@ def parse_args(argv)
97
102
  opts = {
98
103
  store: nil, id: nil, disposition: nil, index: nil,
99
104
  outcome_summary: nil, index_note: nil, no_commit: false, dry_run: false,
105
+ allow_hollow_report: false,
100
106
  session: nil, discard_worktree_changes: false,
101
107
  }
102
108
  i = 0
@@ -113,6 +119,7 @@ def parse_args(argv)
113
119
  when "--discard-worktree-changes" then opts[:discard_worktree_changes] = true
114
120
  when "--no-commit" then opts[:no_commit] = true
115
121
  when "--dry-run" then opts[:dry_run] = true
122
+ when "--allow-hollow-report" then opts[:allow_hollow_report] = true
116
123
  else
117
124
  usage_abort("unknown argument #{arg.inspect}")
118
125
  end
@@ -169,44 +176,41 @@ def resolve_plastic_home_and_scope(store)
169
176
  end
170
177
  end
171
178
 
172
- # Doctor's per-intent structure gate (intent 222), a pre-write step: FAIL refuses the close
173
- # (exit 6, authors nothing), WARN prints an advisory note and proceeds, PASS proceeds
174
- # silently, and an unexpected exception from the gate call itself is rescued and treated as
175
- # a skipped gate (fail-open on a crash: a broken gate must never wedge a close).
176
- #
177
- # `gate:` is an injected seam (mirrors run_disarm's own bridge_reader:/disarm: pattern
178
- # below) defaulting to the real Doctor.new(...).run_intent_check(...) call; tests load this
179
- # script in-process (as test/end_intent_disarm_toctou_test.rb already does for run_disarm)
180
- # and inject a raising stub to prove the fail-open contract, with no eval/ENV/global seam.
179
+ # Backfill (intent 308): write the judgment documents that are missing or still the
180
+ # placeholder from the record (BackfillIntent). Fail-open: a crash here warns and the
181
+ # close proceeds, the same contract as the structure check below.
182
+ def run_backfill(intent_dir, store:, id:, disposition:, summary:)
183
+ result = BackfillIntent.run(intent_dir: intent_dir, store: store, id: id,
184
+ disposition: disposition, summary: summary)
185
+ result[:written].each { |rel| warn "end-intent: backfilled #{rel} from the record" }
186
+ result[:notes].each { |note| warn "end-intent: backfill note: #{note}" }
187
+ rescue StandardError => e
188
+ warn "end-intent: backfill crashed (#{e.message}); proceeding without it"
189
+ end
190
+
191
+ # Doctor's per-intent structure check (intent 222; a self-check since intent 308). Runs
192
+ # after the backfill and REPORTS: every FAIL or WARN check prints its named reason on
193
+ # stderr and the close proceeds. Nothing here exits; a gap end-intent cannot write itself
194
+ # (an unchecked box, a malformed intent file, links out of projection) is the owner's to
195
+ # fix afterward, and doctor --intent keeps reporting it. A crash is rescued the same way.
196
+ # Injected `gate:` seam: tests drive the real Doctor against a hermetic home, or a raising
197
+ # stub to prove the fail-open contract, with no eval/ENV/global seam.
181
198
  #
182
- # Disposition is deliberately NOT passed to the real gate call (intent 222 executor
183
- # decision): the existing outcome-only guard immediately following this call already owns
184
- # disposition-matching (exit 2), and intent_lifecycle_artifacts unconditionally checks
185
- # outcome.md PRESENCE via Bridge.stage_file_present? regardless of disposition, so passing
186
- # it here would make this gate re-check the SAME disposition match the old guard already
187
- # owns, needlessly widening which exit-2 fixtures become exit-6. doctor.rb --intent called
188
- # directly (with --disposition) still exercises the fold-in per D2/acceptance criteria.
189
- def run_structure_gate(id, store:, gate: ->(gate_home, gate_scope) {
199
+ # Disposition is deliberately NOT passed to the real gate call: OutcomeGuard.reason,
200
+ # called right after this in main, already owns disposition-matching, and
201
+ # intent_lifecycle_artifacts checks outcome.md PRESENCE regardless of disposition.
202
+ def run_structure_check(id, store:, gate: ->(gate_home, gate_scope) {
190
203
  require_relative "doctor"
191
204
  Doctor.new(plastic_home: gate_home).run_intent_check(id, store: gate_scope)
192
205
  })
193
206
  gate_home, gate_scope = resolve_plastic_home_and_scope(store)
194
- gate_verdict = gate.call(gate_home, gate_scope)
195
- case gate_verdict[:status]
196
- when "fail"
197
- gate_verdict[:checks].select { |c| c[:status] == "fail" }.each do |c|
198
- detail = Array(c[:details]).any? ? " (#{Array(c[:details]).join("; ")})" : ""
199
- warn "end-intent: structure gate refused: #{c[:name]}: #{c[:message]}#{detail}"
200
- end
201
- exit 6
202
- when "warn"
203
- gate_verdict[:checks].select { |c| c[:status] == "warn" }.each do |c|
204
- detail = Array(c[:details]).any? ? " (#{Array(c[:details]).join("; ")})" : ""
205
- warn "end-intent: structure gate warning (proceeding): #{c[:name]}: #{c[:message]}#{detail}"
206
- end
207
+ verdict = gate.call(gate_home, gate_scope)
208
+ verdict[:checks].select { |c| %w[fail warn].include?(c[:status]) }.each do |c|
209
+ detail = Array(c[:details]).any? ? " (#{Array(c[:details]).join("; ")})" : ""
210
+ warn "end-intent: structure check: #{c[:name]}: #{c[:message]}#{detail}"
207
211
  end
208
212
  rescue StandardError => e
209
- warn "end-intent: structure gate crashed (#{e.message}); proceeding without it"
213
+ warn "end-intent: structure check crashed (#{e.message}); proceeding without it"
210
214
  end
211
215
 
212
216
  # outcome.md guard: see OutcomeGuard.reason (scripts/lib/outcome_guard.rb, extracted intent 222)
@@ -216,7 +220,75 @@ end
216
220
  # Replace the body of the intent file's `## Outcome` section with `summary`,
217
221
  # leaving every other section and the frontmatter untouched. No-op (returns
218
222
  # content unchanged) when summary is blank or the section is not found.
223
+ # 317a S9 (D7): the self-render gate. Renders the delivered screen's sources on
224
+ # the intent's own record and names what would come out hollow. This is intent
225
+ # 308's ONE deliberate exception to report-and-proceed: a hollow DELIVERED
226
+ # close is refused with exit 7 (--allow-hollow-report overrides), because 317
227
+ # closed exactly this way and the owner read a screen full of "not recorded".
228
+ # Abandoned closes and machine-backfilled outcomes (the backfill marker) are
229
+ # exempt: the reader cannot expect labels the writer never had.
230
+ def hollow_report_reason(intent_dir, disposition)
231
+ return nil unless disposition == "delivered"
232
+
233
+ outcome = File.join(intent_dir, "outcome.md")
234
+ return nil unless File.exist?(outcome)
235
+ text = File.read(outcome)
236
+ return nil if text.include?("<!-- backfilled from the record by end-intent on ")
237
+
238
+ # The gate's evidence bar (A7/B7): it judges only records that CLAIM the
239
+ # modern convention - at least one actions/*.md heading carrying an S-label.
240
+ # A terse legacy close with no labeled matrix stays report-and-proceed.
241
+ heading_labels = Dir.glob(File.join(intent_dir, "actions", "*.md")).sort.flat_map do |path|
242
+ ReportScreen.split_by_headings(File.read(path)).filter_map do |heading, _body|
243
+ heading.to_s.sub(/\A#+\s*/, "").split(/[^A-Za-z0-9]+/).find { |tok| tok.match?(/\AS\d+\z/) }
244
+ end
245
+ end.uniq
246
+ return nil if heading_labels.empty?
247
+
248
+ rows = ReportScreen.delivered_rows(intent_dir)
249
+ return "outcome.md ## Delivered renders no rows although a labeled action matrix exists" if rows.empty?
250
+
251
+ needs = ReportScreen.section_of(text, "## Needs you").gsub(/<!--.*?-->/m, "").strip
252
+ if !needs.empty? && needs != "None" && ReportScreen.needs_you_rows(intent_dir).empty?
253
+ return "## Needs you has content that would render as None"
254
+ end
255
+
256
+ labels = rows.map { |r| r[:label] }
257
+ if (labels & heading_labels).empty?
258
+ return "delivered row labels (#{labels.first(3).join(', ')}...) match no action-file heading (#{heading_labels.first(3).join(', ')}...)"
259
+ end
260
+ if rows.all? { |r| ReportScreen.proven_by(intent_dir, r[:label]) == ReportScreen::NOT_RECORDED }
261
+ return "every Proven-by cell renders not recorded although labeled action headings exist"
262
+ end
263
+
264
+ nil
265
+ rescue StandardError => e
266
+ warn "end-intent: hollow-report gate crashed (#{e.message}); proceeding without it"
267
+ nil
268
+ end
269
+
270
+ # 317a S7 (D5, A8): stamp the live lock's run_mode into outcome.md frontmatter
271
+ # INSIDE the close step - after the gate, before the INDEX move and the store
272
+ # commit - so the stamped file rides the close commit and mode survives disarm.
273
+ def stamp_outcome_mode(intent_dir)
274
+ data = Lock.read(intent_dir)
275
+ run_mode = data && data["run_mode"].to_s
276
+ return if run_mode.nil? || run_mode.empty?
277
+
278
+ path = File.join(intent_dir, "outcome.md")
279
+ return unless File.exist?(path)
280
+ text = File.read(path)
281
+ return unless text.start_with?("---")
282
+ head, body = text[3..].split("---", 2)
283
+ return if body.nil? || head.match?(/^mode:/)
284
+
285
+ File.write(path, "---#{head}mode: #{run_mode}\n---#{body}")
286
+ rescue StandardError => e
287
+ warn "end-intent: mode stamp failed (#{e.message}); proceeding without it"
288
+ end
289
+
219
290
  def stamp_outcome_summary(content, summary)
291
+
220
292
  return content if summary.nil? || summary.to_s.strip.empty?
221
293
 
222
294
  lines = content.lines
@@ -459,21 +531,21 @@ end
459
531
 
460
532
  # --- step 5: disarm (D2/D16, intent 188) ------------------------------------
461
533
  #
462
- # Injected seams (D2): bridge_reader defaults to the real Bridge.read (so the
463
- # dirty-worktree check can resolve the code worktree path without disarming
464
- # first), disarm defaults to the real Bridge.disarm_auto. Tests may inject
534
+ # Injected seams (D2, re-pointed by intent 307): worktree_reader defaults to
535
+ # Arm.worktree_block (so the dirty-worktree check can resolve the code worktree
536
+ # path without disarming first), disarm defaults to Arm.disarm. Tests may inject
465
537
  # fakes for in-process unit coverage of this function; the subprocess-driven
466
538
  # end-to-end tests exercise the real defaults against a hermetic tmp home.
467
539
  #
468
540
  # Returns :ok, :dirty (exit 5), or :lock_remains (exit 3). A blank session
469
541
  # (D3's "no lock at all" fallback case) is a clean no-op: :ok, nothing touched.
470
542
  def run_disarm(intent_dir, id, session, discard_worktree_changes:,
471
- bridge_reader: ->(sess, iid) { Bridge.read(sess, intent_id: iid) },
472
- disarm: ->(sess, iid) { Bridge.disarm_auto(sess, intent_id: iid) })
543
+ worktree_reader: ->(dir) { Arm.worktree_block(intent_dir: dir) },
544
+ disarm: ->(sess, dir) { Arm.disarm(intent_dir: dir, session: sess) })
473
545
  return :ok if Bridge.blank?(session)
474
546
 
475
- data = bridge_reader.call(session, id)
476
- worktree_code = data.is_a?(Hash) ? data.dig("worktree", "code") : nil
547
+ block = worktree_reader.call(intent_dir)
548
+ worktree_code = block.is_a?(Hash) ? block["code"] : nil
477
549
 
478
550
  if worktree_code && Dir.exist?(worktree_code)
479
551
  begin
@@ -505,16 +577,15 @@ def run_disarm(intent_dir, id, session, discard_worktree_changes:,
505
577
  end
506
578
  end
507
579
 
508
- disarm.call(session, id)
580
+ disarm.call(session, intent_dir)
509
581
 
510
- # The /tmp bridge is only a cache; the durable lock file is the truth
511
- # (AGENTS.md). `disarm_auto` no-ops entirely when no bridge resolves (a wiped
512
- # /tmp, a resumed job under a new session id), which would otherwise strand a
513
- # committed, terminal intent still holding its lock: the exact stalled
514
- # completion this intent forbids. Pre-flight has already established that this
515
- # session is the owner, a delegate, or the post-takeover owner, so clearing
516
- # the durable lock directly IS safe, PROVIDED the lock actually present here
517
- # is still ours to clear.
582
+ # The durable lock file is the truth (AGENTS.md). Arm.disarm releases as the
583
+ # lock's recorded owner, so a lock a delegate or a post-takeover owner is
584
+ # closing under a different key can survive it; that would strand a committed,
585
+ # terminal intent still holding its lock, the exact stalled completion this
586
+ # intent forbids. Pre-flight has already established that this session is the
587
+ # owner, a delegate, or the post-takeover owner, so clearing the durable lock
588
+ # directly IS safe, PROVIDED the lock actually present here is still ours to clear.
518
589
  #
519
590
  # TOCTOU fix (post-review hardening, intent 188): the first version of this
520
591
  # fallback read the lock's OWN recorded owner and handed it straight back to
@@ -556,8 +627,8 @@ def run_disarm(intent_dir, id, session, discard_worktree_changes:,
556
627
  end
557
628
 
558
629
  unless File.exist?(Lock.path(intent_dir))
559
- if data.nil?
560
- warn "end-intent: no bridge resolved for session #{session.inspect} (intent #{id}); " \
630
+ if block.nil?
631
+ warn "end-intent: no worktree block could be derived for intent #{id}; " \
561
632
  "the delivery lock was cleared directly from disk, but any worktree this intent " \
562
633
  "provisioned was NOT removed. Run /plastic-doctor to check for an orphaned worktree."
563
634
  end
@@ -621,16 +692,10 @@ def main(argv)
621
692
  exit 4
622
693
  end
623
694
 
624
- run_structure_gate(id, store: store)
625
-
626
- reason = OutcomeGuard.reason(intent_dir, disposition)
627
- if reason
628
- warn "end-intent: #{reason}"
629
- exit 2
630
- end
631
-
632
695
  if opts[:dry_run]
633
696
  puts "end-intent: dry run for #{File.basename(intent_dir)} (#{disposition})"
697
+ targets = BackfillIntent.targets_for(intent_dir)
698
+ puts " would backfill: #{targets.empty? ? "(nothing)" : targets.join(", ")}"
634
699
  puts " would stamp ## Outcome: #{opts[:outcome_summary].inspect}" if opts[:outcome_summary]
635
700
  puts " would move INDEX.md entry from ## Active to #{index_target_heading(disposition)} (#{index_path})"
636
701
  puts " would append index note: #{opts[:index_note].inspect}" if opts[:index_note]
@@ -653,9 +718,38 @@ def main(argv)
653
718
  end
654
719
  end
655
720
 
721
+ # 1. Backfill from the record (intent 308), then the structure self-check and the
722
+ # outcome guard, both report-only since 308.
723
+ run_backfill(intent_dir, store: store, id: id, disposition: disposition,
724
+ summary: opts[:outcome_summary])
725
+ run_structure_check(id, store: store)
726
+
727
+ reason = OutcomeGuard.reason(intent_dir, disposition)
728
+ warn "end-intent: outcome.md: #{reason} (proceeding; doctor --intent #{id} reports it)" if reason
729
+
730
+ # 1a2. The self-render gate (317a S9, D7): refuse a hollow delivered close
731
+ # BEFORE any terminal mutation - INDEX, savepoint, and store stay untouched
732
+ # on exit 7 (A9).
733
+ hollow = hollow_report_reason(intent_dir, disposition)
734
+ if hollow
735
+ if opts[:allow_hollow_report]
736
+ warn "end-intent: hollow delivered report overridden (--allow-hollow-report): #{hollow}"
737
+ else
738
+ warn "end-intent: refusing a hollow delivered close: #{hollow}"
739
+ warn "end-intent: write outcome.md's ## Delivered as the labeled | Row | What | table " \
740
+ "matching the action-file headings (templates/outcome.md shows the shape), " \
741
+ "or pass --allow-hollow-report to close anyway"
742
+ exit 7
743
+ end
744
+ end
745
+
746
+ # 1a3. Mode stamp (317a S7, D5): run_mode into outcome frontmatter while the
747
+ # lock still exists, so the stamped file rides the close commit.
748
+ stamp_outcome_mode(intent_dir)
749
+
656
750
  # 1b. Intent-file `## Outcome` summary stamp (no-op unless given).
657
751
  if opts[:outcome_summary]
658
- intent_file = Bridge.intent_file(intent_dir)
752
+ intent_file = Savepoint.intent_file(intent_dir)
659
753
  if File.exist?(intent_file)
660
754
  original = File.read(intent_file)
661
755
  updated = stamp_outcome_summary(original, opts[:outcome_summary])
@@ -682,7 +776,7 @@ def main(argv)
682
776
  end
683
777
 
684
778
  # 3. Savepoint Done bookend (idempotent).
685
- Bridge.append_terminal_savepoint(intent_dir, disposition)
779
+ Savepoint.append_terminal_savepoint(intent_dir, disposition)
686
780
 
687
781
  # 4. Store auto-commit, unless --no-commit. Scoped to this intent's own paths (D17):
688
782
  # never git add -A.
@@ -2,8 +2,9 @@
2
2
  # encoding: UTF-8
3
3
  # frozen_string_literal: true
4
4
 
5
- # exec-worktree (intent 213, group 2) - finish an intent's code worktree: an order
6
- # precondition, then Worktree.finish (delivered merges + removes, abandoned removes only).
5
+ # exec-worktree (intent 213, group 2) - finish an intent's code worktree: the dirty-worktree
6
+ # guard, then Worktree.finish (delivered merges + removes, abandoned removes only). The order
7
+ # precondition it once ran first was removed in 2.0 with the gates (intent 302).
7
8
  #
8
9
  # Usage:
9
10
  # exec-worktree --store <path> --id <intent_id> --home <plastic_home> \
@@ -18,26 +19,17 @@
18
19
  # Steps performed:
19
20
  # 0. Resolve inputs: the intent dir, normalize --home, resolve the calling session
20
21
  # (--session, else CLAUDE_CODE_SESSION_ID, else the existing delivery.lock's own
21
- # recorded owner_session when non-blank, else nil; unlike start-intent, the
22
- # lock-owner fallback IS used here, matching end-intent's teardown resolution order).
23
- # Load the bridge (Bridge.read); a nil bridge means there is nothing to finish (exit
24
- # 0, loud: the worktree this intent provisioned, if any, was NOT removed). No
25
- # worktree.code on the bridge means nothing was provisioned (exit 0).
26
- # 1. The order precondition (delivered only): calls the EXISTING
27
- # Bridge.code_gate_decision predicate exactly as it exists. On an auto bridge that
28
- # has not reached How, this BLOCKS (exit 2). Bridge.code_gate_decision returns nil
29
- # UNCONDITIONALLY on a guided bridge (build.auto != true), so on a guided bridge
30
- # this precondition is ADVISORY ONLY: it always passes and the run proceeds. This
31
- # script is NOT the enforcement point either way; the hook layer
32
- # (scripts/lib/bridge.rb code_gate_decision, wired through scripts/lib/edit_gates.rb)
33
- # remains the actual gate. See scripts/lib/exec_worktree.rb's module doc for why the
34
- # auto-only condition is never relaxed and never duplicated here.
35
- # 2. The dirty-worktree guard (delivered only): git status --porcelain on the code
22
+ # recorded owner_session when non-blank, else nil; the lock-owner fallback IS
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
26
+ # disk means nothing was provisioned (exit 0).
27
+ # 1. The dirty-worktree guard (delivered only): git status --porcelain on the code
36
28
  # worktree. Not clean, or the check itself fails, fails CLOSED (exit 3), mirroring
37
29
  # end-intent's dirty-worktree handling. abandoned skips this guard entirely: the
38
30
  # branch survives and can be reclaimed, so a dirty worktree is not a reason to
39
31
  # refuse.
40
- # 3. Finish: Worktree.finish(bridge_data, home:, runner:, merge: disposition ==
32
+ # 2. Finish: Worktree.finish(bridge_data, home:, runner:, merge: disposition ==
41
33
  # "delivered"). Worktree.finish is fail-open and never raises, so failure is
42
34
  # detected by checking, for a delivered disposition only, whether the worktree
43
35
  # directory still exists on disk afterward (release removes it on success).
@@ -49,8 +41,7 @@
49
41
  # 0 finished (merged or removed per disposition), or nothing to finish (no bridge
50
42
  # resolved, or no code worktree recorded on the bridge)
51
43
  # 1 usage or path-resolution failure
52
- # 2 the order precondition blocked a delivered disposition (auto bridge, How not
53
- # reached)
44
+ # 2 retired in 2.0 (intent 302): was the order precondition; never emitted, never reused
54
45
  # 3 the worktree is not clean, or the merge/removal did not complete (delivered only)
55
46
  # 4 lock or session resolution failure
56
47
 
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: UTF-8
3
+ # frozen_string_literal: true
4
+
5
+ # file-session-intent (intent 301): backfill and close one session day ledger,
6
+ # carrying its open items into a target day.
7
+ #
8
+ # Usage:
9
+ # file-session-intent --day <YYYYMMDD> [--carry-to <YYYYMMDD>] [--store <dir>]
10
+ # [--templates <dir>] [--author <name>]
11
+ #
12
+ # --store defaults to $PLASTIC_HOME/store ($PLASTIC_HOME defaults to ~/.plastic).
13
+ # Prints one line: `filed <day>` or `skipped <day>: closed`. Exit 0 on success
14
+ # and on any filing error (reported on stderr; the next boot tries again), 2 on
15
+ # a usage error.
16
+
17
+ require_relative "lib/session_ledger"
18
+ require_relative "lib/session_backfill"
19
+
20
+ def usage_abort(message)
21
+ warn "file-session-intent: #{message}"
22
+ exit 2
23
+ end
24
+
25
+ def next_value!(argv, i, flag)
26
+ value = argv[i + 1]
27
+ usage_abort("#{flag} needs a value") if value.nil? || value.start_with?("--")
28
+ value
29
+ end
30
+
31
+ opts = { day: nil, carry_to: nil, store: nil, templates: nil, author: "session" }
32
+ i = 0
33
+ while i < ARGV.length
34
+ case ARGV[i]
35
+ when "--day" then opts[:day] = next_value!(ARGV, i, "--day"); i += 1
36
+ when "--carry-to" then opts[:carry_to] = next_value!(ARGV, i, "--carry-to"); i += 1
37
+ when "--store" then opts[:store] = next_value!(ARGV, i, "--store"); i += 1
38
+ when "--templates" then opts[:templates] = next_value!(ARGV, i, "--templates"); i += 1
39
+ when "--author" then opts[:author] = next_value!(ARGV, i, "--author"); i += 1
40
+ else usage_abort("unknown argument #{ARGV[i].inspect}")
41
+ end
42
+ i += 1
43
+ end
44
+
45
+ usage_abort("--day is required") if opts[:day].nil?
46
+ usage_abort("--day must be a real calendar date, YYYYMMDD") unless SessionLedger.valid_day_id?(opts[:day])
47
+ if opts[:carry_to] && !SessionLedger.valid_day_id?(opts[:carry_to])
48
+ usage_abort("--carry-to must be a real calendar date, YYYYMMDD")
49
+ end
50
+
51
+ store = opts[:store] || File.join(File.expand_path(ENV.fetch("PLASTIC_HOME", "~/.plastic")), "store")
52
+ templates = opts[:templates] || File.expand_path("../templates", __dir__)
53
+
54
+ begin
55
+ result = SessionBackfill.file_day(store: store, day: opts[:day], templates: templates,
56
+ author: opts[:author], carry_to: opts[:carry_to])
57
+ puts(result == :closed ? "skipped #{opts[:day]}: closed" : "filed #{opts[:day]}")
58
+ rescue StandardError => e
59
+ warn "file-session-intent: #{opts[:day]} not filed: #{e.class}: #{e.message}"
60
+ end
61
+ exit 0