@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.
- package/PLASTIC.md +101 -294
- package/README.md +5 -6
- package/agents/plastic-advisor.md +18 -17
- package/agents/plastic-enforcer.md +57 -69
- package/agents/plastic-executor.md +34 -20
- package/agents/plastic-faux-advisor.md +18 -18
- package/bin/lib/context_budget.rb +453 -0
- package/bin/plastic-bench +78 -0
- package/config_asks.yml +38 -0
- package/hooks/capture +4 -0
- package/hooks/close +5 -0
- package/hooks/hooks.json +14 -42
- package/hooks/message-display +81 -0
- package/hooks/record +4 -0
- package/hooks/savepoint +5 -5
- package/package.json +3 -2
- package/scripts/agent-report +15 -10
- package/scripts/append-ledger +208 -0
- package/scripts/codex-hook +78 -90
- package/scripts/dashboard.rb +43 -14
- package/scripts/day-summary +53 -0
- package/scripts/doctor.rb +269 -43
- package/scripts/end-intent +170 -76
- package/scripts/exec-worktree +11 -20
- package/scripts/file-session-intent +61 -0
- package/scripts/hook-capture +255 -0
- package/scripts/hook-close +36 -0
- package/scripts/hook-message-display +74 -0
- package/scripts/hook-record +157 -0
- package/scripts/hook-savepoint +45 -0
- package/scripts/hook-session-start +128 -12
- package/scripts/install.rb +8 -6
- package/scripts/intent-screen +77 -0
- package/scripts/lib/agent_models.rb +1 -7
- package/scripts/lib/arm.rb +308 -0
- package/scripts/lib/backfill_intent.rb +316 -0
- package/scripts/lib/bridge.rb +24 -1597
- package/scripts/lib/compact_instructions.rb +56 -0
- package/scripts/lib/day_summary.rb +211 -0
- package/scripts/lib/doctor_core.rb +66 -88
- package/scripts/lib/doctor_session_ledger.rb +158 -0
- package/scripts/lib/exec_worktree.rb +14 -60
- package/scripts/lib/handoff.rb +184 -0
- package/scripts/lib/harness_text.rb +5 -4
- package/scripts/lib/hook_registry.rb +57 -106
- package/scripts/lib/insights.rb +3 -4
- package/scripts/lib/installer_core.rb +136 -41
- package/scripts/lib/intent_screen.rb +309 -0
- package/scripts/lib/intent_screen_ansi.rb +262 -0
- package/scripts/lib/lock.rb +6 -26
- package/scripts/lib/message_display.rb +290 -0
- package/scripts/lib/outcome_guard.rb +7 -5
- package/scripts/lib/project_validator.rb +30 -1
- package/scripts/lib/report_screen.rb +648 -0
- package/scripts/lib/roadmap_queue.rb +3 -3
- package/scripts/lib/roadmap_savepoint.rb +1 -1
- package/scripts/lib/rule_catalog.rb +6 -3
- package/scripts/lib/savepoint.rb +334 -0
- package/scripts/lib/scaffold_intent.rb +13 -179
- package/scripts/lib/screen_paint.rb +276 -0
- package/scripts/lib/session_backfill.rb +184 -0
- package/scripts/lib/session_close.rb +104 -0
- package/scripts/lib/session_git.rb +500 -0
- package/scripts/lib/session_ledger.rb +621 -0
- package/scripts/lib/store_discovery.rb +1 -1
- package/scripts/lib/store_provisioning.rb +1 -1
- package/scripts/maintenance-run +7 -7
- package/scripts/new-intent +63 -4
- package/scripts/plastic-lock +101 -41
- package/scripts/promote-session-item +150 -0
- package/scripts/read-config +3 -0
- package/scripts/rebuild-graph +1 -1
- package/scripts/report-screen +120 -0
- package/scripts/roadmap-next +2 -2
- package/scripts/rollback.rb +48 -3
- package/scripts/savepoint-note +67 -0
- package/scripts/scaffold-intent +37 -63
- package/scripts/session-commit +116 -0
- package/scripts/spawn-preamble +14 -7
- package/scripts/update.rb +7 -3
- package/scripts/write-handoff +60 -0
- package/skills/agent-advisor/SKILL.md +3 -3
- package/skills/agent-advisor/references/advisor-protocol.md +20 -20
- package/skills/auto/SKILL.md +223 -317
- package/skills/auto/evals/evals.json +10 -10
- package/skills/auto/references/agent-architecture.md +102 -90
- package/skills/auto/references/agent-report-contract.md +4 -21
- package/skills/auto/references/end-tail.md +47 -53
- package/skills/auto/references/human-report-contract.md +62 -56
- package/skills/conventions/SKILL.md +1 -3
- package/skills/conventions/references/lifecycle-and-savepoints.md +35 -1
- package/skills/conventions/references/locks-and-worktrees.md +62 -58
- package/skills/conventions/references/maintenance-and-revisions.md +12 -12
- package/skills/dashboard/SKILL.md +1 -1
- package/skills/direct/SKILL.md +66 -0
- package/skills/direct/references/request-signals.md +59 -0
- package/skills/doctor/SKILL.md +58 -11
- package/skills/doctor/report.md +3 -1
- package/skills/install/SKILL.md +11 -9
- package/skills/intent-continuing/SKILL.md +142 -91
- package/skills/intent-continuing/references/boarding-matrix.md +34 -0
- package/skills/intent-continuing/references/context-management.md +3 -3
- package/skills/intent-creating/SKILL.md +2 -2
- package/skills/intent-creating/references/lifecycle.md +1 -1
- package/skills/intent-ending/SKILL.md +34 -37
- package/skills/intent-ending/evals/evals.json +4 -4
- package/skills/intent-executing/SKILL.md +15 -46
- package/skills/intent-executing/plan-reviewer-prompt.md +39 -0
- package/skills/intent-speccing/SKILL.md +108 -51
- package/skills/intent-speccing/evals/evals.json +5 -5
- package/skills/intent-speccing/references/per-section-fill-rules.md +4 -4
- package/skills/intent-speccing/references/self-verify-checklist.md +1 -2
- package/skills/project-creating/SKILL.md +1 -1
- package/skills/releasing/SKILL.md +7 -8
- package/skills/tutorial/SKILL.md +2 -2
- package/skills/tutorial/references/track-1-guided.md +16 -18
- package/skills/tutorial/references/track-2-auto.md +12 -13
- package/skills/tutorial/references/track-3-projects-and-roadmaps.md +2 -2
- package/templates/agents.md +4 -7
- package/templates/config.yml +5 -0
- package/templates/intent-screen.md +17 -0
- package/templates/outcome.md +14 -1
- package/templates/project.yml +7 -0
- package/templates/report-state.md +11 -0
- package/templates/roadmap.md +1 -1
- package/templates/savepoint.md +2 -2
- package/templates/session-intent.md +29 -0
- package/agents/plastic-brainstorming.md +0 -48
- package/agents/plastic-future-intent-researcher.md +0 -35
- package/agents/plastic-intent-curator.md +0 -47
- package/agents/plastic-intent-discovery.md +0 -43
- package/agents/plastic-planner.md +0 -56
- package/agents/plastic-spec-specialist.md +0 -53
- package/hooks/auto-arm +0 -5
- package/hooks/bash-gate +0 -3
- package/hooks/continue +0 -31
- package/hooks/edit-gates +0 -3
- package/hooks/future-intent-check +0 -25
- package/hooks/gate-check +0 -12
- package/hooks/power-tools +0 -8
- package/scripts/hook-auto-arm +0 -51
- package/scripts/hook-bash-gate +0 -78
- package/scripts/hook-code-gate +0 -38
- package/scripts/hook-continue +0 -48
- package/scripts/hook-create-gate +0 -51
- package/scripts/hook-edit-gates +0 -58
- package/scripts/hook-future-intent-check +0 -90
- package/scripts/hook-gate-check +0 -169
- package/scripts/hook-links-gate +0 -54
- package/scripts/hook-lock-gate +0 -47
- package/scripts/hook-power-tools +0 -38
- package/scripts/hook-savepoint-pre +0 -32
- package/scripts/lib/codex_edit_gates.rb +0 -138
- package/scripts/lib/edit_gates.rb +0 -398
- package/scripts/lib/links_gate.rb +0 -140
- package/scripts/lib/qmd_hook.rb +0 -41
- package/scripts/lib/spec_header.rb +0 -83
- package/scripts/lib/start_intent.rb +0 -296
- package/scripts/start-intent +0 -89
- package/skills/_active-intent-gate.md +0 -26
- package/skills/auto/references/tiers.md +0 -100
- package/skills/continuing/SKILL.md +0 -34
- package/skills/continuing/evals/evals.json +0 -91
- package/skills/conventions/references/gates-and-enforcement.md +0 -53
- package/skills/conventions/references/tiers-and-dispatch.md +0 -135
- package/skills/doctor/references/gates-stuck-detection.md +0 -43
- package/skills/intent-brainstorming/SKILL.md +0 -118
- package/skills/intent-brainstorming/evals/evals.json +0 -67
- package/skills/intent-continuing/evals/evals.json +0 -145
- package/skills/intent-discovering/SKILL.md +0 -56
- package/skills/intent-grilling/SKILL.md +0 -108
- package/skills/intent-linking/SKILL.md +0 -128
- package/skills/intent-linking/evals/evals.json +0 -22
- package/skills/intent-linking/references/zettelkasten.md +0 -45
- package/skills/intent-locking/SKILL.md +0 -64
- package/skills/intent-planning/SKILL.md +0 -145
- package/skills/intent-planning/evals/evals.json +0 -81
- package/skills/intent-planning/references/plan-format.md +0 -130
- package/skills/intent-researching/SKILL.md +0 -123
- package/skills/intent-researching/evals/evals.json +0 -22
- package/skills/intent-savepoint/SKILL.md +0 -83
- package/skills/intent-savepoint/references/context-management.md +0 -32
- package/skills/intent-starting/SKILL.md +0 -151
- package/skills/intent-starting/evals/evals.json +0 -117
- package/skills/intent-starting/references/boarding-matrix.md +0 -35
- package/skills/project-continuing/SKILL.md +0 -119
- package/skills/project-continuing/evals/evals.json +0 -100
- package/skills/roadmap-continuing/SKILL.md +0 -89
- package/skills/roadmap-continuing/evals/evals.json +0 -82
- package/skills/skill-creating/SKILL.md +0 -75
- package/skills/skill-creating/evals/evals.json +0 -108
- package/skills/skill-creating/references/agents.md +0 -168
- package/skills/skill-creating/references/defaults-first.md +0 -23
- package/skills/skill-creating/references/evals.md +0 -41
- package/skills/skill-creating/references/hooks.md +0 -251
- package/skills/skill-creating/references/progressive-disclosure.md +0 -176
- package/skills/skill-creating/references/scripts.md +0 -166
- package/skills/skill-creating/references/skills.md +0 -169
- package/skills/skill-creating/scripts/scaffold.rb +0 -313
- package/skills/skill-evaluating/SKILL.md +0 -141
- package/skills/skill-evaluating/assets/eval-template.json +0 -12
- package/skills/skill-evaluating/evals/evals.json +0 -75
- package/skills/skill-evaluating/references/convention-checks.md +0 -76
- package/skills/skill-evaluating/references/eval-methodology.md +0 -154
- package/skills/store-curating/SKILL.md +0 -64
- package/skills/store-curating/evals/evals.json +0 -38
- package/skills/store-indexing/SKILL.md +0 -93
- package/skills/store-indexing/evals/evals.json +0 -22
- package/skills/store-indexing/references/zettelkasten-linking.md +0 -32
- package/skills/store-provisioning/SKILL.md +0 -55
- /package/skills/{project-continuing → intent-continuing}/references/board-fill.md +0 -0
- /package/skills/{roadmap-continuing → intent-continuing}/references/liveness-ranking.md +0 -0
- /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
|