@zalom/plastic 1.14.1 → 2.0.0-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (200) 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 +9 -49
  13. package/hooks/record +4 -0
  14. package/hooks/savepoint +5 -5
  15. package/package.json +3 -2
  16. package/scripts/agent-report +8 -9
  17. package/scripts/append-ledger +208 -0
  18. package/scripts/codex-hook +78 -90
  19. package/scripts/dashboard.rb +4 -4
  20. package/scripts/day-summary +53 -0
  21. package/scripts/doctor.rb +106 -43
  22. package/scripts/end-intent +77 -76
  23. package/scripts/exec-worktree +11 -20
  24. package/scripts/file-session-intent +61 -0
  25. package/scripts/hook-capture +242 -0
  26. package/scripts/hook-close +36 -0
  27. package/scripts/hook-record +149 -0
  28. package/scripts/hook-savepoint +45 -0
  29. package/scripts/hook-session-start +106 -12
  30. package/scripts/install.rb +8 -6
  31. package/scripts/lib/agent_models.rb +1 -7
  32. package/scripts/lib/arm.rb +283 -0
  33. package/scripts/lib/backfill_intent.rb +316 -0
  34. package/scripts/lib/bridge.rb +24 -1597
  35. package/scripts/lib/compact_instructions.rb +56 -0
  36. package/scripts/lib/day_summary.rb +206 -0
  37. package/scripts/lib/doctor_core.rb +65 -88
  38. package/scripts/lib/doctor_session_ledger.rb +106 -0
  39. package/scripts/lib/exec_worktree.rb +14 -60
  40. package/scripts/lib/handoff.rb +184 -0
  41. package/scripts/lib/harness_text.rb +5 -4
  42. package/scripts/lib/hook_registry.rb +44 -107
  43. package/scripts/lib/insights.rb +3 -4
  44. package/scripts/lib/installer_core.rb +104 -40
  45. package/scripts/lib/lock.rb +6 -26
  46. package/scripts/lib/outcome_guard.rb +7 -5
  47. package/scripts/lib/project_validator.rb +30 -1
  48. package/scripts/lib/roadmap_queue.rb +3 -3
  49. package/scripts/lib/roadmap_savepoint.rb +1 -1
  50. package/scripts/lib/rule_catalog.rb +6 -3
  51. package/scripts/lib/savepoint.rb +320 -0
  52. package/scripts/lib/scaffold_intent.rb +13 -179
  53. package/scripts/lib/session_backfill.rb +184 -0
  54. package/scripts/lib/session_close.rb +104 -0
  55. package/scripts/lib/session_git.rb +469 -0
  56. package/scripts/lib/session_ledger.rb +497 -0
  57. package/scripts/lib/store_discovery.rb +1 -1
  58. package/scripts/lib/store_provisioning.rb +1 -1
  59. package/scripts/maintenance-run +7 -7
  60. package/scripts/new-intent +63 -4
  61. package/scripts/plastic-lock +94 -41
  62. package/scripts/promote-session-item +150 -0
  63. package/scripts/read-config +3 -0
  64. package/scripts/rebuild-graph +1 -1
  65. package/scripts/roadmap-next +2 -2
  66. package/scripts/rollback.rb +48 -3
  67. package/scripts/scaffold-intent +37 -63
  68. package/scripts/session-commit +116 -0
  69. package/scripts/spawn-preamble +5 -5
  70. package/scripts/update.rb +7 -3
  71. package/scripts/write-handoff +60 -0
  72. package/skills/agent-advisor/SKILL.md +3 -3
  73. package/skills/agent-advisor/references/advisor-protocol.md +20 -20
  74. package/skills/auto/SKILL.md +218 -317
  75. package/skills/auto/evals/evals.json +10 -10
  76. package/skills/auto/references/agent-architecture.md +102 -90
  77. package/skills/auto/references/agent-report-contract.md +4 -21
  78. package/skills/auto/references/end-tail.md +47 -53
  79. package/skills/auto/references/human-report-contract.md +8 -8
  80. package/skills/conventions/SKILL.md +1 -3
  81. package/skills/conventions/references/lifecycle-and-savepoints.md +35 -1
  82. package/skills/conventions/references/locks-and-worktrees.md +47 -55
  83. package/skills/conventions/references/maintenance-and-revisions.md +12 -12
  84. package/skills/dashboard/SKILL.md +1 -1
  85. package/skills/direct/SKILL.md +66 -0
  86. package/skills/direct/references/request-signals.md +59 -0
  87. package/skills/doctor/SKILL.md +58 -11
  88. package/skills/doctor/report.md +3 -1
  89. package/skills/install/SKILL.md +11 -9
  90. package/skills/intent-continuing/SKILL.md +132 -89
  91. package/skills/intent-continuing/references/boarding-matrix.md +34 -0
  92. package/skills/intent-continuing/references/context-management.md +2 -2
  93. package/skills/intent-creating/SKILL.md +2 -2
  94. package/skills/intent-creating/references/lifecycle.md +1 -1
  95. package/skills/intent-ending/SKILL.md +28 -37
  96. package/skills/intent-ending/evals/evals.json +4 -4
  97. package/skills/intent-executing/SKILL.md +12 -49
  98. package/skills/intent-executing/plan-reviewer-prompt.md +39 -0
  99. package/skills/intent-speccing/SKILL.md +108 -51
  100. package/skills/intent-speccing/evals/evals.json +5 -5
  101. package/skills/intent-speccing/references/per-section-fill-rules.md +4 -4
  102. package/skills/intent-speccing/references/self-verify-checklist.md +1 -2
  103. package/skills/project-creating/SKILL.md +1 -1
  104. package/skills/releasing/SKILL.md +7 -8
  105. package/skills/tutorial/SKILL.md +2 -2
  106. package/skills/tutorial/references/track-1-guided.md +16 -18
  107. package/skills/tutorial/references/track-2-auto.md +12 -13
  108. package/skills/tutorial/references/track-3-projects-and-roadmaps.md +2 -2
  109. package/templates/agents.md +4 -7
  110. package/templates/config.yml +5 -0
  111. package/templates/project.yml +7 -0
  112. package/templates/roadmap.md +1 -1
  113. package/templates/savepoint.md +2 -2
  114. package/templates/session-intent.md +29 -0
  115. package/agents/plastic-brainstorming.md +0 -48
  116. package/agents/plastic-future-intent-researcher.md +0 -35
  117. package/agents/plastic-intent-curator.md +0 -47
  118. package/agents/plastic-intent-discovery.md +0 -43
  119. package/agents/plastic-planner.md +0 -56
  120. package/agents/plastic-spec-specialist.md +0 -53
  121. package/hooks/auto-arm +0 -5
  122. package/hooks/bash-gate +0 -3
  123. package/hooks/continue +0 -31
  124. package/hooks/edit-gates +0 -3
  125. package/hooks/future-intent-check +0 -25
  126. package/hooks/gate-check +0 -12
  127. package/hooks/power-tools +0 -8
  128. package/scripts/hook-auto-arm +0 -51
  129. package/scripts/hook-bash-gate +0 -78
  130. package/scripts/hook-code-gate +0 -38
  131. package/scripts/hook-continue +0 -48
  132. package/scripts/hook-create-gate +0 -51
  133. package/scripts/hook-edit-gates +0 -58
  134. package/scripts/hook-future-intent-check +0 -90
  135. package/scripts/hook-gate-check +0 -169
  136. package/scripts/hook-links-gate +0 -54
  137. package/scripts/hook-lock-gate +0 -47
  138. package/scripts/hook-power-tools +0 -38
  139. package/scripts/hook-savepoint-pre +0 -32
  140. package/scripts/lib/codex_edit_gates.rb +0 -138
  141. package/scripts/lib/edit_gates.rb +0 -398
  142. package/scripts/lib/links_gate.rb +0 -140
  143. package/scripts/lib/qmd_hook.rb +0 -41
  144. package/scripts/lib/spec_header.rb +0 -83
  145. package/scripts/lib/start_intent.rb +0 -296
  146. package/scripts/start-intent +0 -89
  147. package/skills/_active-intent-gate.md +0 -26
  148. package/skills/auto/references/tiers.md +0 -100
  149. package/skills/continuing/SKILL.md +0 -34
  150. package/skills/continuing/evals/evals.json +0 -91
  151. package/skills/conventions/references/gates-and-enforcement.md +0 -53
  152. package/skills/conventions/references/tiers-and-dispatch.md +0 -135
  153. package/skills/doctor/references/gates-stuck-detection.md +0 -43
  154. package/skills/intent-brainstorming/SKILL.md +0 -118
  155. package/skills/intent-brainstorming/evals/evals.json +0 -67
  156. package/skills/intent-continuing/evals/evals.json +0 -145
  157. package/skills/intent-discovering/SKILL.md +0 -56
  158. package/skills/intent-grilling/SKILL.md +0 -108
  159. package/skills/intent-linking/SKILL.md +0 -128
  160. package/skills/intent-linking/evals/evals.json +0 -22
  161. package/skills/intent-linking/references/zettelkasten.md +0 -45
  162. package/skills/intent-locking/SKILL.md +0 -64
  163. package/skills/intent-planning/SKILL.md +0 -145
  164. package/skills/intent-planning/evals/evals.json +0 -81
  165. package/skills/intent-planning/references/plan-format.md +0 -130
  166. package/skills/intent-researching/SKILL.md +0 -123
  167. package/skills/intent-researching/evals/evals.json +0 -22
  168. package/skills/intent-savepoint/SKILL.md +0 -83
  169. package/skills/intent-savepoint/references/context-management.md +0 -32
  170. package/skills/intent-starting/SKILL.md +0 -151
  171. package/skills/intent-starting/evals/evals.json +0 -117
  172. package/skills/intent-starting/references/boarding-matrix.md +0 -35
  173. package/skills/project-continuing/SKILL.md +0 -119
  174. package/skills/project-continuing/evals/evals.json +0 -100
  175. package/skills/roadmap-continuing/SKILL.md +0 -89
  176. package/skills/roadmap-continuing/evals/evals.json +0 -82
  177. package/skills/skill-creating/SKILL.md +0 -75
  178. package/skills/skill-creating/evals/evals.json +0 -108
  179. package/skills/skill-creating/references/agents.md +0 -168
  180. package/skills/skill-creating/references/defaults-first.md +0 -23
  181. package/skills/skill-creating/references/evals.md +0 -41
  182. package/skills/skill-creating/references/hooks.md +0 -251
  183. package/skills/skill-creating/references/progressive-disclosure.md +0 -176
  184. package/skills/skill-creating/references/scripts.md +0 -166
  185. package/skills/skill-creating/references/skills.md +0 -169
  186. package/skills/skill-creating/scripts/scaffold.rb +0 -313
  187. package/skills/skill-evaluating/SKILL.md +0 -141
  188. package/skills/skill-evaluating/assets/eval-template.json +0 -12
  189. package/skills/skill-evaluating/evals/evals.json +0 -75
  190. package/skills/skill-evaluating/references/convention-checks.md +0 -76
  191. package/skills/skill-evaluating/references/eval-methodology.md +0 -154
  192. package/skills/store-curating/SKILL.md +0 -64
  193. package/skills/store-curating/evals/evals.json +0 -38
  194. package/skills/store-indexing/SKILL.md +0 -93
  195. package/skills/store-indexing/evals/evals.json +0 -22
  196. package/skills/store-indexing/references/zettelkasten-linking.md +0 -32
  197. package/skills/store-provisioning/SKILL.md +0 -55
  198. /package/skills/{project-continuing → intent-continuing}/references/board-fill.md +0 -0
  199. /package/skills/{roadmap-continuing → intent-continuing}/references/liveness-ranking.md +0 -0
  200. /package/skills/{intent-brainstorming → intent-speccing}/references/design-principles.md +0 -0
@@ -0,0 +1,316 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
4
+ require "fileutils"
5
+ require "time"
6
+ require_relative "scaffold_intent"
7
+ require_relative "savepoint"
8
+ require_relative "intent_validator"
9
+ require_relative "worktree"
10
+
11
+ # BackfillIntent (intent 308) - write the four judgment documents of an intent from its
12
+ # record at intent end. The record is what an intent already carries by the time it
13
+ # closes: the intent file (`## Intent`, `### Decisions`, `## Insights`), checklist.md,
14
+ # and the diff on its own code worktree. Every section written here is a verbatim copy
15
+ # or a mechanical rendering of one of those; a section with no source keeps the
16
+ # template's own stub text, so nothing here invents prose. The one value that comes from
17
+ # the caller is outcome.md's `disposition:`, the closer's explicit assertion (spec D11).
18
+ #
19
+ # A target is backfilled only when it is missing or still the scaffold placeholder: the
20
+ # first line is the sentinel AND the body under it is nothing but the template
21
+ # (`edited_sentinel?`). A sentinel over hand-written lines counts as real. actions/ is
22
+ # backfilled as ACTION_1.md only when `Savepoint.has_real_action?` is false. A real file
23
+ # is never touched and there is no force flag. Every written file carries a marker
24
+ # comment on the line after its title, never the sentinel, so every reader treats it as
25
+ # real. A zero-byte spec.md or plan.md reads as real here exactly as it does in
26
+ # `Savepoint.stage_file_present?` (spec D1), so the doctor surfaces never disagree.
27
+ #
28
+ # Pure and dependency-injected like ScaffoldIntent: never exits, never reads ARGV or ENV
29
+ # (only the ambient Dir.home default), and takes the git seam as `runner:` so tests drive
30
+ # it in process with a fake runner.
31
+ module BackfillIntent
32
+ module_function
33
+
34
+ TARGETS = ["spec.md", "plan.md", "actions/ACTION_1.md", "outcome.md"].freeze
35
+ TEMPLATED = ["spec.md", "plan.md", "outcome.md"].freeze
36
+ MARKER_PREFIX = "<!-- backfilled from the record by end-intent on "
37
+ ITEM_RE = /\A\s*- \[( |x|X)\] /.freeze
38
+ TITLE_RE = /\A# /.freeze
39
+ STUB_SECTIONS = {
40
+ "## Goals" => "- ...\n", "## Non-Goals" => "- ...\n",
41
+ "## Approach" => "(the chosen approach, in prose)\n",
42
+ "## Alternatives Considered" => "- <alternative>: not chosen because ...\n",
43
+ }.freeze
44
+
45
+ def marker(now)
46
+ "#{MARKER_PREFIX}#{now.utc.iso8601} -->"
47
+ end
48
+
49
+ # --- templates ---------------------------------------------------------------------
50
+
51
+ # { "spec.md" => text, ... } for the templates that exist, else {}.
52
+ def load_templates(templates_dir: nil, home: Dir.home)
53
+ dir = templates_dir || ScaffoldIntent.resolve_templates_dir(home: home)
54
+ return {} if dir.nil?
55
+
56
+ TEMPLATED.each_with_object({}) do |name, acc|
57
+ path = File.join(dir, name)
58
+ acc[name] = File.read(path) if File.exist?(path)
59
+ end
60
+ end
61
+
62
+ # --- target selection --------------------------------------------------------------
63
+
64
+ # True iff `path` starts with the sentinel and its body carries a non-blank, non-title
65
+ # line that the template for this file does not: someone wrote under the sentinel.
66
+ # With no template known, any body line at all means edited.
67
+ def edited_sentinel?(path, template_text, known_lines: [])
68
+ lines = File.read(path).lines
69
+ return false unless lines.first.to_s.chomp == Savepoint::PLACEHOLDER_SENTINEL
70
+
71
+ body = lines.drop(1).map(&:strip).reject { |l| l.empty? || l.match?(TITLE_RE) }
72
+ return body.any? if template_text.nil?
73
+
74
+ known = template_text.lines.map(&:strip).reject(&:empty?) + known_lines.map(&:strip)
75
+ (body - known).any?
76
+ rescue StandardError
77
+ true
78
+ end
79
+
80
+ # { targets: [rel], edited: [rel] }: the targets that need writing, and the sentinel
81
+ # files that were left alone because their body was edited.
82
+ def classify(intent_dir, templates: {}, known_lines: [])
83
+ targets = []
84
+ edited = []
85
+ TEMPLATED.each do |rel|
86
+ path = File.join(intent_dir, rel)
87
+ next if Savepoint.stage_file_present?(path)
88
+
89
+ if File.exist?(path) && edited_sentinel?(path, templates[rel], known_lines: known_lines)
90
+ edited << rel
91
+ else
92
+ targets << rel
93
+ end
94
+ end
95
+ unless Savepoint.has_real_action?(intent_dir)
96
+ action = File.join(intent_dir, "actions", "ACTION_1.md")
97
+ if File.exist?(action) && !Savepoint.stage_file_present?(action) && edited_sentinel?(action, nil)
98
+ edited << "actions/ACTION_1.md"
99
+ elsif !File.exist?(action) || !Savepoint.stage_file_present?(action)
100
+ targets << "actions/ACTION_1.md"
101
+ end
102
+ end
103
+ { targets: TARGETS & targets, edited: TARGETS & edited }
104
+ end
105
+
106
+ def targets_for(intent_dir, templates_dir: nil, home: Dir.home)
107
+ classify(intent_dir, templates: load_templates(templates_dir: templates_dir, home: home))[:targets]
108
+ end
109
+
110
+ # --- reading the record (pure over file contents) ---------------------------------
111
+
112
+ def strip_edges(lines)
113
+ ScaffoldIntent.strip_blank_edges(lines)
114
+ end
115
+
116
+ def section_lines(text, heading)
117
+ body = ScaffoldIntent.sections_from(text)[heading]
118
+ return [] if body.nil?
119
+
120
+ strip_edges(body.lines.map(&:rstrip)).reject { |l| l.start_with?("<!--") && l.end_with?("-->") }
121
+ end
122
+
123
+ # Every GFM checkbox item of checklist.md with its section and done flag. A continuation
124
+ # line (indented, not an item, not a heading, not a table row) stays attached to the item
125
+ # above it so a wrapped item round-trips verbatim.
126
+ def checklist_items(text)
127
+ items = []
128
+ section = nil
129
+ text.each_line do |raw|
130
+ line = raw.rstrip
131
+ if line.start_with?("## ")
132
+ section = line
133
+ elsif (m = ITEM_RE.match(line))
134
+ items << { line: line, done: m[1] != " ", section: section }
135
+ elsif items.any? && line.start_with?(" ") && !line.start_with?(" |") && section == items.last[:section]
136
+ items.last[:line] = "#{items.last[:line]}\n#{line}"
137
+ end
138
+ end
139
+ items
140
+ end
141
+
142
+ def session_log_lines(text)
143
+ section_lines(text, "## Session Log").select { |l| l.start_with?("|") }
144
+ end
145
+
146
+ # [record, nil] or [nil, message] when the intent file is missing. `record[:notes]`
147
+ # names every source that was absent; an absent source never stops the write.
148
+ def read_record(intent_dir)
149
+ intent_file = Savepoint.intent_file(intent_dir)
150
+ return [nil, "the intent file is missing at #{intent_file}"] unless File.exist?(intent_file)
151
+
152
+ content = File.read(intent_file)
153
+ fm = IntentValidator.parse_frontmatter_text(content)
154
+ name = fm.is_a?(Hash) ? fm["intent"].to_s.strip : ""
155
+ notes = []
156
+ if name.empty?
157
+ name = File.basename(intent_dir)
158
+ notes << "the intent file has no frontmatter intent name; the directory name stands in"
159
+ end
160
+ decisions, = ScaffoldIntent.extract_decisions(content)
161
+ notes << "the intent file has no ### Decisions list; spec.md says so" if decisions.nil?
162
+
163
+ checklist = File.join(intent_dir, "checklist.md")
164
+ checklist_text = ""
165
+ if Savepoint.stage_file_present?(checklist)
166
+ checklist_text = File.read(checklist)
167
+ else
168
+ notes << "checklist.md is missing or still a placeholder; the item sections say so"
169
+ end
170
+
171
+ [{
172
+ name: name,
173
+ intent_body: section_lines(content, "## Intent"),
174
+ decisions: decisions,
175
+ insights: section_lines(content, "## Insights"),
176
+ items: checklist_items(checklist_text),
177
+ session_log: session_log_lines(checklist_text),
178
+ notes: notes,
179
+ }, nil]
180
+ end
181
+
182
+ # --- rendering (pure) ---------------------------------------------------------------
183
+
184
+ def block(lines, empty)
185
+ lines.empty? ? "#{empty}\n" : "#{lines.join("\n")}\n"
186
+ end
187
+
188
+ def template_stub(template_text, heading)
189
+ body = template_text && ScaffoldIntent.sections_from(template_text)[heading]
190
+ body = STUB_SECTIONS.fetch(heading, "") if body.nil? || body.strip.empty?
191
+ "#{body.rstrip}\n"
192
+ end
193
+
194
+ def render(record, disposition:, summary:, verification:, now:, templates: {})
195
+ mark = marker(now)
196
+ items = record[:items].map { |i| i[:line] }
197
+ done = record[:items].select { |i| i[:done] }.map { |i| i[:line] }
198
+ open = record[:items].reject { |i| i[:done] }.map { |i| i[:line] }
199
+ decisions = record[:decisions].nil? ? "(none recorded in the intent file)\n" : record[:decisions]
200
+ decisions += "\n" unless decisions.end_with?("\n")
201
+
202
+ spec = +"# Spec: #{record[:name]}\n#{mark}\n\n"
203
+ spec << "## Problem\n#{block(record[:intent_body], '(not recorded in the intent file)')}\n"
204
+ STUB_SECTIONS.each_key { |h| spec << "#{h}\n#{template_stub(templates['spec.md'], h)}\n" }
205
+ spec << "## Decisions\n#{decisions}\n"
206
+ spec << "## Acceptance Criteria\n#{block(items, '(no checklist items recorded)')}\n"
207
+ spec << "## Open Questions\nNone\n"
208
+
209
+ plan = +"# Plan: #{record[:name]}\n#{mark}\n\n"
210
+ plan << "## Goal\n#{record[:name]}\n\n"
211
+ plan << "## Steps\n#{block(items, '(no checklist items recorded)')}\n"
212
+ plan << "## Notes\n#{block(record[:insights], '(no insights recorded)')}"
213
+
214
+ action = +"# ACTION_1: #{record[:name]}\n#{mark}\n\n"
215
+ action << "## Items\n#{block(items, '(no checklist items recorded)')}\n"
216
+ action << "## Session Log\n#{block(record[:session_log], '(none)')}"
217
+
218
+ summary_text = summary.to_s.strip
219
+ summary_text = "(no summary given at close)" if summary_text.empty?
220
+ outcome = +"---\ndisposition: #{disposition}\n---\n# Outcome: #{record[:name]}\n#{mark}\n\n"
221
+ outcome << "## Summary\n#{summary_text}\n\n"
222
+ outcome << "## Delivered\n#{block(done, '(no completed checklist items recorded)')}\n"
223
+ outcome << "## Verification\n#{verification.to_s.strip.empty? ? "(no diff available)\n" : verification}\n"
224
+ outcome << "## Follow-ups\n#{block(open, 'None')}"
225
+
226
+ { "spec.md" => spec, "plan.md" => plan, "actions/ACTION_1.md" => action, "outcome.md" => outcome }
227
+ end
228
+
229
+ # --- the diff, from this intent's own code worktree only -----------------------------
230
+
231
+ # The provisioned code worktree for this intent when it exists on disk, else nil. Never
232
+ # the current directory's repo: a store-only intent closed from an unrelated checkout
233
+ # must not report that checkout's diff.
234
+ def worktree_dir(store:, id:, intent_dir:, home: Dir.home)
235
+ wt_home = Worktree.home_from_store(store) || home
236
+ slug = Worktree.slug_for_store(store, home: wt_home)
237
+ return nil if slug.nil?
238
+
239
+ intent_slug = File.basename(intent_dir).split("--", 2).last
240
+ code = Worktree.paths(slug: slug, intent_id: id, intent_slug: intent_slug, home: wt_home)["code"]
241
+ code && Dir.exist?(code) ? code : nil
242
+ end
243
+
244
+ def verification_body(store:, id:, intent_dir:, home:, runner:)
245
+ repo = worktree_dir(store: store, id: id, intent_dir: intent_dir, home: home)
246
+ return "Diffstat unavailable: this intent provisioned no code worktree\n" if repo.nil?
247
+
248
+ base = ScaffoldIntent.detect_base_branch(repo, runner: runner)
249
+ return "Diffstat unavailable: no base branch could be detected (no origin/HEAD, main, or master)\n" if base.nil?
250
+
251
+ stat, err = ScaffoldIntent.diffstat(repo, base, runner: runner)
252
+ return "Diffstat unavailable: #{err}\n" if stat.nil?
253
+
254
+ "Diffstat against #{base}:\n```\n#{stat}#{stat.end_with?("\n") ? "" : "\n"}```\n"
255
+ rescue StandardError => e
256
+ "Diffstat unavailable: #{e.message}\n"
257
+ end
258
+
259
+ # --- writing, atomic inside the intent dir -------------------------------------------
260
+
261
+ def write_atomic(intent_dir, relative, content)
262
+ target = File.join(intent_dir, relative)
263
+ FileUtils.mkdir_p(File.dirname(target))
264
+ tmp = File.join(File.dirname(target), ".filing-#{File.basename(target)}")
265
+ File.write(tmp, content)
266
+ File.rename(tmp, target)
267
+ target
268
+ end
269
+
270
+ # A `.filing-*` left by an earlier crash would otherwise ride into the store commit.
271
+ def remove_stale_filing(intent_dir)
272
+ Dir.glob([File.join(intent_dir, ".filing-*"), File.join(intent_dir, "actions", ".filing-*")]).each do |f|
273
+ File.delete(f) if File.file?(f)
274
+ end
275
+ end
276
+
277
+ # Returns { written: [rel], skipped: [rel], notes: [String] }. `skipped` lists the
278
+ # targets that already carry real content (an edited sentinel among them, named in
279
+ # `notes`). `notes` names every absent source; an absent source never stops the write.
280
+ # One savepoint line, `Exec backfilled <list>`, records the fact (idempotent per list).
281
+ def run(intent_dir:, store:, id:, disposition:, summary: nil, home: Dir.home,
282
+ runner: Worktree::ShellRunner.new, templates_dir: nil, now: Time.now)
283
+ result = { written: [], skipped: [], notes: [] }
284
+ remove_stale_filing(intent_dir)
285
+
286
+ templates = load_templates(templates_dir: templates_dir, home: home)
287
+ record, err = read_record(intent_dir)
288
+ if record.nil?
289
+ result[:notes] << err
290
+ result[:skipped] = TARGETS - classify(intent_dir, templates: templates)[:targets]
291
+ return result
292
+ end
293
+
294
+ classified = classify(intent_dir, templates: templates, known_lines: [record[:name]])
295
+ targets = classified[:targets]
296
+ result[:skipped] = TARGETS - targets
297
+ classified[:edited].each do |rel|
298
+ result[:notes] << "#{rel} carries a sentinel over hand-written content; left untouched"
299
+ end
300
+ result[:notes].concat(record[:notes])
301
+ return result if targets.empty?
302
+
303
+ verification = nil
304
+ if targets.include?("outcome.md")
305
+ verification = verification_body(store: store, id: id, intent_dir: intent_dir, home: home, runner: runner)
306
+ end
307
+ docs = render(record, disposition: disposition, summary: summary, verification: verification,
308
+ now: now, templates: templates)
309
+ targets.each do |rel|
310
+ write_atomic(intent_dir, rel, docs.fetch(rel))
311
+ result[:written] << rel
312
+ end
313
+ Savepoint.append_savepoint_line(intent_dir, "Exec", "backfilled #{result[:written].join(', ')}", now)
314
+ result
315
+ end
316
+ end