space-architect 1.1.0 → 1.2.0
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.
- checksums.yaml +4 -4
- data/lib/space_architect/architect_mission.rb +345 -36
- data/lib/space_architect/cli/architect.rb +188 -2
- data/lib/space_architect/templates/architect.md.erb +4 -0
- data/lib/space_architect/templates/brief.md.erb +43 -0
- data/lib/space_architect/templates/iteration.md.erb +26 -18
- data/lib/space_architect/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 62235ba54045a8883eaaaced2fef7c231d45ad6cb363dda9256ec65692b6023b
|
|
4
|
+
data.tar.gz: 7d90b91f3a247c20cc273c332807c65c8e500dc62bffaec1a35076263eb969a2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 042c966a4dbf2075abaeaffb23ad9e426a4421ffcd9621ff238d2f074771e5d65bc7030775fe1733dde40030dc0aff37b84c35e13a035ac29705e3e42667c96e
|
|
7
|
+
data.tar.gz: f633823970cefde990131c1cce4a415bd8f767030120438ae7bd76220653ab0c42580352de48ab2c34b82f5fd4cc597c6ba14410603e66de6a3dd86f89e197ee
|
|
@@ -17,6 +17,26 @@ module SpaceArchitect
|
|
|
17
17
|
# from the appended-after-freeze sections (Builder Prompt/Report/Verdict).
|
|
18
18
|
FROZEN_BOUNDARY = /^## Builder Prompt/
|
|
19
19
|
|
|
20
|
+
# Sections the architect writes (and the CLI commits) via `architect section`.
|
|
21
|
+
# Acceptance Criteria is intentionally absent — it is set by `architect freeze`,
|
|
22
|
+
# the one code path that creates the freeze commit. Builder Report has its own
|
|
23
|
+
# command (`architect evidence`) because it is transcribed verbatim from scratch.
|
|
24
|
+
# `frozen: true` sections live above the freeze boundary and are refused once frozen.
|
|
25
|
+
SECTIONS = {
|
|
26
|
+
"grounds" => { heading: "## Grounds", message: "grounds", frozen: true },
|
|
27
|
+
"specification" => { heading: "## Specification", message: "specification", frozen: true },
|
|
28
|
+
"prompt" => { heading: "## Builder Prompt", message: "dispatched", frozen: false },
|
|
29
|
+
"verdict" => { heading: "## Verdict", message: "verdict", frozen: false }
|
|
30
|
+
}.freeze
|
|
31
|
+
|
|
32
|
+
# The fixed top-level section headings. Section boundaries are detected against
|
|
33
|
+
# this set (not any "## " line), so a verbatim Builder Report containing its own
|
|
34
|
+
# "## " headings cannot fool the parser.
|
|
35
|
+
KNOWN_HEADINGS = [
|
|
36
|
+
"## Grounds", "## Specification", "## Acceptance Criteria",
|
|
37
|
+
"## Builder Prompt", "## Builder Report", "## Verdict"
|
|
38
|
+
].freeze
|
|
39
|
+
|
|
20
40
|
def initialize(space:)
|
|
21
41
|
@space = space
|
|
22
42
|
end
|
|
@@ -133,7 +153,221 @@ module SpaceArchitect
|
|
|
133
153
|
sha
|
|
134
154
|
end
|
|
135
155
|
|
|
136
|
-
|
|
156
|
+
# Scaffold the durable, section-numbered mission brief at architecture/BRIEF.md
|
|
157
|
+
# and commit it. The brief is the stable cross-iteration address space iterations
|
|
158
|
+
# cite as "BRIEF §N"; it lives outside the per-iteration freeze region.
|
|
159
|
+
def brief_new!(force: false)
|
|
160
|
+
brief_path = space.path.join("architecture", "BRIEF.md")
|
|
161
|
+
if brief_path.exist? && !force
|
|
162
|
+
raise Error, "architecture/BRIEF.md already exists — edit it directly (idempotent guard), or pass --force to overwrite"
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
FileUtils.mkdir_p(brief_path.dirname)
|
|
166
|
+
brief_path.write(render_brief)
|
|
167
|
+
git_run("-C", space.path.to_s, "add", "architecture/BRIEF.md")
|
|
168
|
+
git_run("-C", space.path.to_s, "commit", "-m", "Add mission brief") if staged_changes?
|
|
169
|
+
brief_path
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Write one section of the iteration file and commit it with the canonical
|
|
173
|
+
# per-section message, in one call. Refuses to write a frozen section
|
|
174
|
+
# (Grounds/Specification) once the iteration is frozen. Acceptance Criteria is
|
|
175
|
+
# NOT writable here (use freeze); Builder Report is not here (use evidence).
|
|
176
|
+
def write_section!(iteration, section, body:, append: false, lane: nil)
|
|
177
|
+
spec = SECTIONS[section]
|
|
178
|
+
unless spec
|
|
179
|
+
raise Error,
|
|
180
|
+
"Unknown section '#{section}' — one of: #{SECTIONS.keys.join(', ')}. " \
|
|
181
|
+
"(Acceptance Criteria is set by `architect freeze`; Builder Report by `architect evidence`.)"
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
entry = slice_entry(iteration)
|
|
185
|
+
rel = entry["file"]
|
|
186
|
+
path = space.path.join(rel)
|
|
187
|
+
raise Error, "#{rel} does not exist — run `architect new #{iteration}` first" unless path.exist?
|
|
188
|
+
|
|
189
|
+
if spec[:frozen] && entry["freeze_sha"]
|
|
190
|
+
raise Error,
|
|
191
|
+
"#{spec[:heading]} is frozen for #{iteration} (freeze #{entry["freeze_sha"][0, 8]}) — " \
|
|
192
|
+
"frozen sections are read-only after the freeze commit. Open a new iteration to change the contract."
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
block = lane ? "### #{lane}\n\n#{body.strip}" : body.strip
|
|
196
|
+
path.write(replace_section_body(path.read, spec[:heading], block, append: append))
|
|
197
|
+
|
|
198
|
+
nn = format("%02d", entry["ordinal"] || 0)
|
|
199
|
+
git_run("-C", space.path.to_s, "add", rel)
|
|
200
|
+
committed = staged_changes?
|
|
201
|
+
git_run("-C", space.path.to_s, "commit", "-m", "I#{nn}: #{spec[:message]}") if committed
|
|
202
|
+
|
|
203
|
+
head, = git_capture("-C", space.path.to_s, "rev-parse", "HEAD")
|
|
204
|
+
diffstat, = committed ? git_capture("-C", space.path.to_s, "show", "--stat", "--format=", "HEAD") : [""]
|
|
205
|
+
{ section: section, heading: spec[:heading], sha: head.strip, committed: committed, diffstat: diffstat.strip }
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Transcribe a lane's scratch report (build/<id>[-<lane>]/report.md) VERBATIM into
|
|
209
|
+
# the Builder Report section and commit. Byte-for-byte: no summarization, no judgment.
|
|
210
|
+
def transcribe_evidence!(iteration, lane: nil)
|
|
211
|
+
entry = slice_entry(iteration)
|
|
212
|
+
rel = entry["file"]
|
|
213
|
+
path = space.path.join(rel)
|
|
214
|
+
raise Error, "#{rel} does not exist — run `architect new #{iteration}` first" unless path.exist?
|
|
215
|
+
|
|
216
|
+
id = iteration_id(entry)
|
|
217
|
+
report = space.path.join("build", lane ? "#{id}-#{lane}" : id, "report.md")
|
|
218
|
+
raise Error, "builder report not found: #{report}" unless report.exist?
|
|
219
|
+
raw = report.read
|
|
220
|
+
raise Error, "builder report is empty: #{report}" if raw.strip.empty?
|
|
221
|
+
|
|
222
|
+
block = lane ? "### #{lane}\n\n#{raw.rstrip}" : raw.rstrip
|
|
223
|
+
path.write(replace_section_body(path.read, "## Builder Report", block, append: !lane.nil?))
|
|
224
|
+
|
|
225
|
+
nn = format("%02d", entry["ordinal"] || 0)
|
|
226
|
+
git_run("-C", space.path.to_s, "add", rel)
|
|
227
|
+
git_run("-C", space.path.to_s, "commit", "-m", "I#{nn}: evidence") if staged_changes?
|
|
228
|
+
head, = git_capture("-C", space.path.to_s, "rev-parse", "HEAD")
|
|
229
|
+
|
|
230
|
+
status_line = raw.lines.reverse_each.find { |l| l.strip.start_with?("STATUS:") }&.strip
|
|
231
|
+
{ sha: head.strip, lines: raw.lines.count, status_line: status_line, lane: lane }
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# Read the Acceptance Criteria section text, by default from the freeze commit
|
|
235
|
+
# (so the architect quotes the frozen gates, never a drifted working copy).
|
|
236
|
+
def acceptance_criteria(iteration, ref: :freeze)
|
|
237
|
+
entry = slice_entry(iteration)
|
|
238
|
+
rel = entry["file"]
|
|
239
|
+
ref = entry["freeze_sha"] if ref == :freeze
|
|
240
|
+
text =
|
|
241
|
+
if ref
|
|
242
|
+
out, _, st = git_capture("-C", space.path.to_s, "show", "#{ref}:#{rel}")
|
|
243
|
+
raise Error, "could not read #{rel} at #{ref}" unless st.success?
|
|
244
|
+
out
|
|
245
|
+
else
|
|
246
|
+
space.path.join(rel).read
|
|
247
|
+
end
|
|
248
|
+
section_body(text, "## Acceptance Criteria")
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# Integrate ONE architect-judged-passing lane: commit the builder's working tree on
|
|
252
|
+
# the lane branch, then merge --no-ff into the repo's lane/<id> integration branch.
|
|
253
|
+
# Runs NO gates and makes NO pass/fail decision. Refuses a mechanically-failing lane
|
|
254
|
+
# (builder commits / out-of-bounds) and aborts cleanly on a merge conflict.
|
|
255
|
+
def merge_lane!(iteration, lane, message: nil)
|
|
256
|
+
entry = slice_entry(iteration)
|
|
257
|
+
lane_entry = (entry["lanes"] || []).find { |l| l["name"] == lane }
|
|
258
|
+
raise Error, "No lane '#{lane}' recorded for iteration '#{iteration}'" unless lane_entry
|
|
259
|
+
|
|
260
|
+
checks = lane_mechanical_checks(entry, lane_entry)
|
|
261
|
+
if checks[:no_builder_commits] == false
|
|
262
|
+
raise Error, "Lane '#{lane}' has builder commits — the worktree is tampered (hard rule 7). Reset and re-dispatch; do not merge."
|
|
263
|
+
end
|
|
264
|
+
if checks[:in_bounds] == false
|
|
265
|
+
raise Error, "Lane '#{lane}' wrote outside its declared touch set — out-of-bounds fails the lane. Reset and re-dispatch."
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
repo = lane_entry["repo"]
|
|
269
|
+
repo_path = space.path.join("repos", repo)
|
|
270
|
+
id = iteration_id(entry)
|
|
271
|
+
wt_path = space.path.join(lane_entry["worktree"] || "build/#{id}-#{lane}/wt")
|
|
272
|
+
raise Error, "Worktree directory does not exist: #{wt_path}" unless wt_path.exist?
|
|
273
|
+
base_sha = lane_entry["base_sha"]
|
|
274
|
+
lane_branch = "lane/#{id}-#{lane}"
|
|
275
|
+
integration_branch = "lane/#{id}"
|
|
276
|
+
|
|
277
|
+
status_out, = git_capture("-C", wt_path.to_s, "status", "--porcelain")
|
|
278
|
+
raise Error, "Lane '#{lane}' worktree has no changes to integrate." if status_out.strip.empty?
|
|
279
|
+
|
|
280
|
+
git_run("-C", wt_path.to_s, "add", "-A")
|
|
281
|
+
git_run("-C", wt_path.to_s, "commit", "-m", message || "lane #{lane}: integrate")
|
|
282
|
+
|
|
283
|
+
_o, _e, exists = git_capture("-C", repo_path.to_s, "rev-parse", "--verify", "--quiet", integration_branch)
|
|
284
|
+
if exists.success?
|
|
285
|
+
git_run("-C", repo_path.to_s, "checkout", integration_branch)
|
|
286
|
+
else
|
|
287
|
+
git_run("-C", repo_path.to_s, "checkout", "-b", integration_branch, base_sha)
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
_mo, merr, mst = git_capture("-C", repo_path.to_s, "merge", "--no-ff", lane_branch, "-m", "Merge #{lane_branch}")
|
|
291
|
+
unless mst.success?
|
|
292
|
+
conflicts, = git_capture("-C", repo_path.to_s, "diff", "--name-only", "--diff-filter=U")
|
|
293
|
+
git_capture("-C", repo_path.to_s, "merge", "--abort")
|
|
294
|
+
raise Error,
|
|
295
|
+
"Merge conflict integrating lane '#{lane}' (#{conflicts.split.join(", ")}) — the lane plan was " \
|
|
296
|
+
"not disjoint = a spec defect. Kill the conflicting lane and re-spec; do not hand-resolve. #{merr.strip}"
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
merge_sha, = git_capture("-C", repo_path.to_s, "rev-parse", "HEAD")
|
|
300
|
+
diffstat, = git_capture("-C", repo_path.to_s, "diff", "--stat", "#{base_sha}..HEAD")
|
|
301
|
+
|
|
302
|
+
update_architect_block do |b|
|
|
303
|
+
(b["iterations"] || []).each do |s|
|
|
304
|
+
next unless s["name"] == iteration
|
|
305
|
+
(s["lanes"] || []).each { |l| l["integration_branch"] = integration_branch if l["name"] == lane }
|
|
306
|
+
end
|
|
307
|
+
b
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
{ lane: lane, repo: repo, integration_branch: integration_branch,
|
|
311
|
+
merge_sha: merge_sha.strip, base_sha: base_sha, diffstat: diffstat.strip, gates_run: false }
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
# Loop merge_lane! over the architect-supplied passing set, in order. Stops on the
|
|
315
|
+
# first conflict (a disjointness defect). Never decides which lanes pass.
|
|
316
|
+
def integrate!(iteration, lanes:, teardown: false)
|
|
317
|
+
raise Error, "No lanes given to integrate" if lanes.nil? || lanes.empty?
|
|
318
|
+
|
|
319
|
+
merged = []
|
|
320
|
+
lanes.each do |lane|
|
|
321
|
+
merged << merge_lane!(iteration, lane)
|
|
322
|
+
rescue Error => e
|
|
323
|
+
done = merged.map { |m| m[:lane] }.join(", ")
|
|
324
|
+
raise Error, "Integrated #{done.empty? ? "(none)" : done} then stopped at '#{lane}': #{e.message}"
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
if teardown
|
|
328
|
+
id = iteration_id(slice_entry(iteration))
|
|
329
|
+
merged.each do |m|
|
|
330
|
+
worktree_remove(iteration, m[:lane])
|
|
331
|
+
git_capture("-C", space.path.join("repos", m[:repo]).to_s, "branch", "-d", "lane/#{id}-#{m[:lane]}")
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
merged
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
# Run the iteration's frozen Acceptance Criteria gate commands and stream raw
|
|
338
|
+
# stdout/stderr + exit codes. A path-resolving RUNNER ONLY — no threshold
|
|
339
|
+
# comparison, no PASS/FAIL. The verdict is the architect reading this output.
|
|
340
|
+
def run_gates(iteration, lane: nil)
|
|
341
|
+
entry = slice_entry(iteration)
|
|
342
|
+
freeze_sha = entry["freeze_sha"]
|
|
343
|
+
raise Error, "Iteration '#{iteration}' is not frozen — freeze before running gates." unless freeze_sha
|
|
344
|
+
rel = entry["file"]
|
|
345
|
+
|
|
346
|
+
text, _, st = git_capture("-C", space.path.to_s, "show", "#{freeze_sha}:#{rel}")
|
|
347
|
+
raise Error, "could not read frozen #{rel} at #{freeze_sha[0, 8]}" unless st.success?
|
|
348
|
+
commands = acceptance_criteria_commands(text)
|
|
349
|
+
raise Error, "no gate commands found in the frozen Acceptance Criteria of #{rel}" if commands.empty?
|
|
350
|
+
|
|
351
|
+
lanes = entry["lanes"] || []
|
|
352
|
+
dir =
|
|
353
|
+
if lane
|
|
354
|
+
le = lanes.find { |l| l["name"] == lane }
|
|
355
|
+
raise Error, "No lane '#{lane}' recorded for iteration '#{iteration}'" unless le
|
|
356
|
+
space.path.join(le["worktree"] || "build/#{iteration_id(entry)}-#{lane}/wt")
|
|
357
|
+
else
|
|
358
|
+
repo = lanes.first&.dig("repo")
|
|
359
|
+
raise Error, "No lane/repo recorded for '#{iteration}' — cannot resolve a directory to run gates in" unless repo
|
|
360
|
+
space.path.join("repos", repo)
|
|
361
|
+
end
|
|
362
|
+
raise Error, "directory does not exist: #{dir}" unless dir.exist?
|
|
363
|
+
|
|
364
|
+
commands.map do |row|
|
|
365
|
+
out, err, status = Open3.capture3(row[:command], chdir: dir.to_s)
|
|
366
|
+
{ ac: row[:ac], command: row[:command], stdout: out, stderr: err, exit_code: status.exitstatus, dir: dir }
|
|
367
|
+
end
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
def worktree_add(repo, iteration, lane, base: nil, harness: "claude-code", model: nil, variant: false, effort: nil, touch: nil)
|
|
137
371
|
if harness.to_s == "opencode" && (model.nil? || model == Harness::CLAUDE_DEFAULT_MODEL)
|
|
138
372
|
raise Error,
|
|
139
373
|
"Pass --model when using --harness opencode " \
|
|
@@ -177,6 +411,7 @@ module SpaceArchitect
|
|
|
177
411
|
"variant" => variant
|
|
178
412
|
}
|
|
179
413
|
lane_entry["effort"] = effort if effort
|
|
414
|
+
lane_entry["touch_set"] = Array(touch) if touch && !Array(touch).empty?
|
|
180
415
|
lanes << lane_entry
|
|
181
416
|
s["lanes"] = lanes
|
|
182
417
|
end
|
|
@@ -295,41 +530,8 @@ module SpaceArchitect
|
|
|
295
530
|
|
|
296
531
|
def verify(iteration)
|
|
297
532
|
entry = slice_entry(iteration)
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
lanes = entry["lanes"] || []
|
|
301
|
-
|
|
302
|
-
lanes.map do |lane|
|
|
303
|
-
lane_name = lane["name"]
|
|
304
|
-
base_sha = lane["base_sha"]
|
|
305
|
-
wt_path = space.path.join(lane["worktree"] || "build/#{iteration_id(entry)}-#{lane_name}/wt")
|
|
306
|
-
touch_set = lane["touch_set"] || []
|
|
307
|
-
|
|
308
|
-
checks = {}
|
|
309
|
-
|
|
310
|
-
# (a) frozen sections of the iteration file untouched since freeze
|
|
311
|
-
checks[:frozen_untouched] = if freeze_sha && rel
|
|
312
|
-
!frozen_region_changed?(freeze_sha, rel)
|
|
313
|
-
end
|
|
314
|
-
|
|
315
|
-
# (b) no builder commits in the worktree
|
|
316
|
-
log_out, = git_capture("-C", wt_path.to_s, "log", "#{base_sha}..")
|
|
317
|
-
checks[:no_builder_commits] = log_out.strip.empty?
|
|
318
|
-
|
|
319
|
-
# (c) builder's scratch report exists and is non-empty
|
|
320
|
-
report = space.path.join("build", "#{iteration_id(entry)}-#{lane_name}", "report.md")
|
|
321
|
-
checks[:report_exists] = report.exist? && !report.read.strip.empty?
|
|
322
|
-
|
|
323
|
-
# (d) in-bounds: changed paths ⊆ touch_set (nil if no touch_set recorded)
|
|
324
|
-
checks[:in_bounds] = if touch_set.empty?
|
|
325
|
-
nil
|
|
326
|
-
else
|
|
327
|
-
status_out, = git_capture("-C", wt_path.to_s, "status", "--porcelain")
|
|
328
|
-
changed = status_out.lines.map { |l| l[3..].strip }
|
|
329
|
-
changed.all? { |f| touch_set.any? { |g| File.fnmatch(g, f) } }
|
|
330
|
-
end
|
|
331
|
-
|
|
332
|
-
{ lane: lane_name, repo: lane["repo"], checks: checks }
|
|
533
|
+
(entry["lanes"] || []).map do |lane|
|
|
534
|
+
{ lane: lane["name"], repo: lane["repo"], checks: lane_mechanical_checks(entry, lane) }
|
|
333
535
|
end
|
|
334
536
|
end
|
|
335
537
|
|
|
@@ -394,6 +596,107 @@ module SpaceArchitect
|
|
|
394
596
|
frozen_region(old) != frozen_region(current)
|
|
395
597
|
end
|
|
396
598
|
|
|
599
|
+
# The four per-lane post-flight checks, shared by `verify` (reports) and
|
|
600
|
+
# `merge_lane!` (refuses on failure) so the two can never drift.
|
|
601
|
+
def lane_mechanical_checks(entry, lane)
|
|
602
|
+
freeze_sha = entry["freeze_sha"]
|
|
603
|
+
rel = entry["file"]
|
|
604
|
+
lane_name = lane["name"]
|
|
605
|
+
base_sha = lane["base_sha"]
|
|
606
|
+
wt_path = space.path.join(lane["worktree"] || "build/#{iteration_id(entry)}-#{lane_name}/wt")
|
|
607
|
+
touch_set = lane["touch_set"] || []
|
|
608
|
+
|
|
609
|
+
checks = {}
|
|
610
|
+
|
|
611
|
+
# (a) frozen sections of the iteration file untouched since freeze
|
|
612
|
+
checks[:frozen_untouched] = (!frozen_region_changed?(freeze_sha, rel) if freeze_sha && rel)
|
|
613
|
+
|
|
614
|
+
# (b) no builder commits in the worktree
|
|
615
|
+
log_out, = git_capture("-C", wt_path.to_s, "log", "#{base_sha}..")
|
|
616
|
+
checks[:no_builder_commits] = log_out.strip.empty?
|
|
617
|
+
|
|
618
|
+
# (c) builder's scratch report exists and is non-empty
|
|
619
|
+
report = space.path.join("build", "#{iteration_id(entry)}-#{lane_name}", "report.md")
|
|
620
|
+
checks[:report_exists] = report.exist? && !report.read.strip.empty?
|
|
621
|
+
|
|
622
|
+
# (d) in-bounds: changed paths ⊆ touch_set (nil if no touch_set recorded)
|
|
623
|
+
checks[:in_bounds] = if touch_set.empty?
|
|
624
|
+
nil
|
|
625
|
+
else
|
|
626
|
+
status_out, = git_capture("-C", wt_path.to_s, "status", "--porcelain")
|
|
627
|
+
changed = status_out.lines.map { |l| l[3..].to_s.strip }
|
|
628
|
+
changed.all? { |f| touch_set.any? { |g| File.fnmatch(g, f) } }
|
|
629
|
+
end
|
|
630
|
+
|
|
631
|
+
checks
|
|
632
|
+
end
|
|
633
|
+
|
|
634
|
+
# Replace (or, with append:, extend) the body of a "## Heading" section, leaving
|
|
635
|
+
# every other section byte-untouched. Append replaces a placeholder body (only a
|
|
636
|
+
# template comment) the first time, then stacks subsections after it.
|
|
637
|
+
def replace_section_body(text, heading, new_block, append:)
|
|
638
|
+
lines = text.lines
|
|
639
|
+
start = lines.index { |l| l.chomp == heading }
|
|
640
|
+
raise Error, "section heading '#{heading}' not found in iteration file" unless start
|
|
641
|
+
|
|
642
|
+
finish = ((start + 1)...lines.length).find { |i| KNOWN_HEADINGS.include?(lines[i].chomp) } || lines.length
|
|
643
|
+
body = lines[(start + 1)...finish].join
|
|
644
|
+
|
|
645
|
+
new_body =
|
|
646
|
+
if append && !placeholder_body?(body)
|
|
647
|
+
"#{body.strip}\n\n#{new_block.strip}"
|
|
648
|
+
else
|
|
649
|
+
new_block.strip
|
|
650
|
+
end
|
|
651
|
+
|
|
652
|
+
prefix = lines[0..start].join.rstrip
|
|
653
|
+
suffix = lines[finish..].to_a.join.strip
|
|
654
|
+
parts = [prefix, "", new_body]
|
|
655
|
+
parts += ["", suffix] unless suffix.empty?
|
|
656
|
+
"#{parts.join("\n")}\n"
|
|
657
|
+
end
|
|
658
|
+
|
|
659
|
+
# A section body is a placeholder when it holds nothing but a leading HTML
|
|
660
|
+
# comment (the scaffold's guidance) and whitespace.
|
|
661
|
+
def placeholder_body?(body)
|
|
662
|
+
body.strip.sub(/\A<!--.*?-->/m, "").strip.empty?
|
|
663
|
+
end
|
|
664
|
+
|
|
665
|
+
# The text between "## Heading" and the next "## " heading (nil if absent).
|
|
666
|
+
def section_body(text, heading)
|
|
667
|
+
lines = text.lines
|
|
668
|
+
start = lines.index { |l| l.chomp == heading }
|
|
669
|
+
return nil unless start
|
|
670
|
+
finish = ((start + 1)...lines.length).find { |i| KNOWN_HEADINGS.include?(lines[i].chomp) } || lines.length
|
|
671
|
+
lines[(start + 1)...finish].join.strip
|
|
672
|
+
end
|
|
673
|
+
|
|
674
|
+
# Parse the Acceptance Criteria markdown table into [{ac:, command:}]. Reads the
|
|
675
|
+
# Command column by header name (so an added "Brief §" column doesn't shift it);
|
|
676
|
+
# strips surrounding backticks and unescapes \| inside a cell.
|
|
677
|
+
def acceptance_criteria_commands(text)
|
|
678
|
+
body = section_body(text, "## Acceptance Criteria")
|
|
679
|
+
return [] unless body
|
|
680
|
+
rows = body.lines.map(&:strip).select { |l| l.start_with?("|") }
|
|
681
|
+
return [] if rows.length < 2
|
|
682
|
+
|
|
683
|
+
header = split_md_row(rows[0])
|
|
684
|
+
cmd_idx = header.index { |c| c.downcase == "command" } || 1
|
|
685
|
+
ac_idx = header.index { |c| c.downcase.start_with?("ac") } || 0
|
|
686
|
+
|
|
687
|
+
rows[2..].to_a.filter_map do |line|
|
|
688
|
+
cells = split_md_row(line)
|
|
689
|
+
command = cells[cmd_idx].to_s.gsub(/\A`+|`+\z/, "").strip
|
|
690
|
+
next if command.empty?
|
|
691
|
+
{ ac: cells[ac_idx].to_s.strip, command: command }
|
|
692
|
+
end
|
|
693
|
+
end
|
|
694
|
+
|
|
695
|
+
def split_md_row(line)
|
|
696
|
+
inner = line.strip.sub(/\A\|/, "").sub(/\|\z/, "")
|
|
697
|
+
inner.split(/(?<!\\)\|/).map { |c| c.strip.gsub('\\|', "|") }
|
|
698
|
+
end
|
|
699
|
+
|
|
397
700
|
def staged_changes?
|
|
398
701
|
_o, _e, st = git_capture("-C", space.path.to_s, "diff", "--cached", "--quiet")
|
|
399
702
|
!st.success? # --quiet exits non-zero when there are staged differences
|
|
@@ -405,6 +708,12 @@ module SpaceArchitect
|
|
|
405
708
|
render_template("architect.md.erb")
|
|
406
709
|
end
|
|
407
710
|
|
|
711
|
+
def render_brief
|
|
712
|
+
@_title = space.data["title"] || space.id
|
|
713
|
+
@_repos = space.repos
|
|
714
|
+
render_template("brief.md.erb")
|
|
715
|
+
end
|
|
716
|
+
|
|
408
717
|
def render_iteration(ordinal_nn, name)
|
|
409
718
|
@_ordinal = "I#{ordinal_nn}"
|
|
410
719
|
@_name = name
|
|
@@ -107,6 +107,12 @@ module SpaceArchitect
|
|
|
107
107
|
mission = ArchitectMission.new(space: sp)
|
|
108
108
|
sha = mission.freeze!(iteration)
|
|
109
109
|
terminal.say "Frozen #{iteration} at #{sha}"
|
|
110
|
+
ac = mission.acceptance_criteria(iteration)
|
|
111
|
+
unless ac.to_s.strip.empty?
|
|
112
|
+
terminal.say ""
|
|
113
|
+
terminal.say "Frozen Acceptance Criteria (quote these verbatim when judging):"
|
|
114
|
+
terminal.say ac
|
|
115
|
+
end
|
|
110
116
|
CLI.record_outcome(Outcome.new(exit_code: 0))
|
|
111
117
|
end
|
|
112
118
|
end
|
|
@@ -195,6 +201,176 @@ module SpaceArchitect
|
|
|
195
201
|
end
|
|
196
202
|
end
|
|
197
203
|
|
|
204
|
+
class Section < Dry::CLI::Command
|
|
205
|
+
include GlobalOptions
|
|
206
|
+
include Helpers
|
|
207
|
+
|
|
208
|
+
desc "Write a section of the iteration file and commit it (one call)"
|
|
209
|
+
argument :iteration, required: true, desc: "Iteration name"
|
|
210
|
+
argument :section, required: true, desc: "Section: grounds, specification, prompt, verdict"
|
|
211
|
+
argument :space, required: false, desc: "Space identifier (default: $PWD)"
|
|
212
|
+
option :from, default: nil, desc: "Read the section body from this file"
|
|
213
|
+
option :body, default: nil, desc: "Inline section body (one-liners)"
|
|
214
|
+
option :stdin, type: :boolean, default: false, desc: "Read the section body from stdin"
|
|
215
|
+
option :append, type: :boolean, default: false, desc: "Append a ### <lane> subsection instead of replacing"
|
|
216
|
+
option :lane, default: nil, desc: "Lane name for an appended ### subsection"
|
|
217
|
+
|
|
218
|
+
def call(iteration:, section:, space: nil, from: nil, body: nil, stdin: false, append: false, lane: nil, **opts)
|
|
219
|
+
setup_terminal(**opts.slice(:color, :colors))
|
|
220
|
+
handle_errors do
|
|
221
|
+
content = read_section_body(from: from, body: body, stdin: stdin)
|
|
222
|
+
render(store.find(space)) do |sp|
|
|
223
|
+
mission = ArchitectMission.new(space: sp)
|
|
224
|
+
res = mission.write_section!(iteration, section, body: content, append: append, lane: lane)
|
|
225
|
+
if res[:committed]
|
|
226
|
+
terminal.say "Committed #{res[:heading]} → #{res[:sha][0, 8]}"
|
|
227
|
+
terminal.say res[:diffstat] unless res[:diffstat].empty?
|
|
228
|
+
else
|
|
229
|
+
terminal.say "#{res[:heading]} written — no change to commit"
|
|
230
|
+
end
|
|
231
|
+
CLI.record_outcome(Outcome.new(exit_code: 0))
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
private
|
|
237
|
+
|
|
238
|
+
def read_section_body(from:, body:, stdin:)
|
|
239
|
+
return File.read(from) if from
|
|
240
|
+
return body if body
|
|
241
|
+
return $stdin.read if stdin
|
|
242
|
+
raise SpaceArchitect::Error, "provide the section body via --from <file>, --body <text>, or --stdin"
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
class Evidence < Dry::CLI::Command
|
|
247
|
+
include GlobalOptions
|
|
248
|
+
include Helpers
|
|
249
|
+
|
|
250
|
+
desc "Transcribe a lane's scratch report VERBATIM into Builder Report and commit"
|
|
251
|
+
argument :iteration, required: true, desc: "Iteration name"
|
|
252
|
+
argument :space, required: false, desc: "Space identifier (default: $PWD)"
|
|
253
|
+
option :lane, default: nil, desc: "Lane name (per-lane subsection; omit for a single-lane iteration)"
|
|
254
|
+
|
|
255
|
+
def call(iteration:, space: nil, lane: nil, **opts)
|
|
256
|
+
setup_terminal(**opts.slice(:color, :colors))
|
|
257
|
+
handle_errors do
|
|
258
|
+
render(store.find(space)) do |sp|
|
|
259
|
+
mission = ArchitectMission.new(space: sp)
|
|
260
|
+
res = mission.transcribe_evidence!(iteration, lane: lane)
|
|
261
|
+
terminal.say "Transcribed #{res[:lines]} lines → #{res[:sha][0, 8]}"
|
|
262
|
+
terminal.say "Builder STATUS: #{res[:status_line]}" if res[:status_line]
|
|
263
|
+
terminal.say "Now rule on the builder's PHASE 0 disagreements in the Verdict (a later session)."
|
|
264
|
+
CLI.record_outcome(Outcome.new(exit_code: 0))
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
class Merge < Dry::CLI::Command
|
|
271
|
+
include GlobalOptions
|
|
272
|
+
include Helpers
|
|
273
|
+
|
|
274
|
+
desc "Integrate ONE judged-passing lane (merges --no-ff; runs no gates, makes no verdict)"
|
|
275
|
+
argument :iteration, required: true, desc: "Iteration name"
|
|
276
|
+
argument :lane, required: true, desc: "Lane name (architect-judged passing)"
|
|
277
|
+
argument :space, required: false, desc: "Space identifier (default: $PWD)"
|
|
278
|
+
option :message, default: nil, desc: "Commit message for the lane's working-tree changes"
|
|
279
|
+
|
|
280
|
+
def call(iteration:, lane:, space: nil, message: nil, **opts)
|
|
281
|
+
setup_terminal(**opts.slice(:color, :colors))
|
|
282
|
+
handle_errors do
|
|
283
|
+
render(store.find(space)) do |sp|
|
|
284
|
+
mission = ArchitectMission.new(space: sp)
|
|
285
|
+
r = mission.merge_lane!(iteration, lane, message: message)
|
|
286
|
+
terminal.say "Merged #{lane} → #{r[:integration_branch]} (#{r[:merge_sha][0, 8]})"
|
|
287
|
+
terminal.say r[:diffstat] unless r[:diffstat].empty?
|
|
288
|
+
terminal.say "Gates NOT run — run `architect gate #{iteration}` against the integration branch."
|
|
289
|
+
CLI.record_outcome(Outcome.new(exit_code: 0))
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
class Integrate < Dry::CLI::Command
|
|
296
|
+
include GlobalOptions
|
|
297
|
+
include Helpers
|
|
298
|
+
|
|
299
|
+
desc "Integrate the architect-supplied set of passing lanes, in order (stops on conflict)"
|
|
300
|
+
argument :iteration, required: true, desc: "Iteration name"
|
|
301
|
+
argument :space, required: false, desc: "Space identifier (default: $PWD)"
|
|
302
|
+
option :lanes, required: true, desc: "Comma-separated passing lane names (you decide the set)"
|
|
303
|
+
option :teardown, type: :boolean, default: false, desc: "Remove worktrees + delete lane branches after merge"
|
|
304
|
+
|
|
305
|
+
def call(iteration:, space: nil, lanes:, teardown: false, **opts)
|
|
306
|
+
setup_terminal(**opts.slice(:color, :colors))
|
|
307
|
+
handle_errors do
|
|
308
|
+
render(store.find(space)) do |sp|
|
|
309
|
+
mission = ArchitectMission.new(space: sp)
|
|
310
|
+
lane_names = lanes.to_s.split(",").map(&:strip).reject(&:empty?)
|
|
311
|
+
results = mission.integrate!(iteration, lanes: lane_names, teardown: teardown)
|
|
312
|
+
results.each do |r|
|
|
313
|
+
terminal.say "Merged #{r[:lane]} → #{r[:integration_branch]} (#{r[:merge_sha][0, 8]})"
|
|
314
|
+
end
|
|
315
|
+
terminal.say "Gates NOT run — run `architect gate #{iteration}`; the verdict is the next session's."
|
|
316
|
+
CLI.record_outcome(Outcome.new(exit_code: 0))
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
class Gate < Dry::CLI::Command
|
|
323
|
+
include GlobalOptions
|
|
324
|
+
include Helpers
|
|
325
|
+
|
|
326
|
+
desc "Run the frozen Acceptance Criteria gate commands and stream raw output (no PASS/FAIL)"
|
|
327
|
+
argument :iteration, required: true, desc: "Iteration name"
|
|
328
|
+
argument :lane, required: false, desc: "Run in a lane worktree (default: the integration repo)"
|
|
329
|
+
argument :space, required: false, desc: "Space identifier (default: $PWD)"
|
|
330
|
+
|
|
331
|
+
def call(iteration:, lane: nil, space: nil, **opts)
|
|
332
|
+
setup_terminal(**opts.slice(:color, :colors))
|
|
333
|
+
handle_errors do
|
|
334
|
+
render(store.find(space)) do |sp|
|
|
335
|
+
mission = ArchitectMission.new(space: sp)
|
|
336
|
+
results = mission.run_gates(iteration, lane: lane)
|
|
337
|
+
results.each do |r|
|
|
338
|
+
terminal.say ""
|
|
339
|
+
terminal.say "── #{r[:ac].empty? ? "(gate)" : r[:ac]}: #{r[:command]} (exit #{r[:exit_code]})"
|
|
340
|
+
terminal.say r[:stdout].rstrip unless r[:stdout].strip.empty?
|
|
341
|
+
terminal.say r[:stderr].rstrip unless r[:stderr].strip.empty?
|
|
342
|
+
end
|
|
343
|
+
terminal.say ""
|
|
344
|
+
terminal.say "Raw gate output above — the PASS/FAIL/INVALID verdict is yours, read against the frozen thresholds."
|
|
345
|
+
CLI.record_outcome(Outcome.new(exit_code: 0))
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
module Brief
|
|
352
|
+
class New < Dry::CLI::Command
|
|
353
|
+
include GlobalOptions
|
|
354
|
+
include Helpers
|
|
355
|
+
|
|
356
|
+
desc "Scaffold the durable mission brief (architecture/BRIEF.md)"
|
|
357
|
+
argument :space, required: false, desc: "Space identifier (default: $PWD)"
|
|
358
|
+
option :force, type: :boolean, default: false, desc: "Overwrite an existing BRIEF.md"
|
|
359
|
+
|
|
360
|
+
def call(space: nil, force: false, **opts)
|
|
361
|
+
setup_terminal(**opts.slice(:color, :colors))
|
|
362
|
+
handle_errors do
|
|
363
|
+
render(store.find(space)) do |sp|
|
|
364
|
+
mission = ArchitectMission.new(space: sp)
|
|
365
|
+
path = mission.brief_new!(force: force)
|
|
366
|
+
terminal.say "Brief ready: #{terminal.path(path)}"
|
|
367
|
+
CLI.record_outcome(Outcome.new(exit_code: 0))
|
|
368
|
+
end
|
|
369
|
+
end
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
|
|
198
374
|
module Worktree
|
|
199
375
|
class Add < Dry::CLI::Command
|
|
200
376
|
include GlobalOptions
|
|
@@ -208,14 +384,16 @@ module SpaceArchitect
|
|
|
208
384
|
option :harness, default: "claude-code", desc: "Harness (claude-code, opencode)"
|
|
209
385
|
option :model, default: nil, desc: "Model (required for opencode)"
|
|
210
386
|
option :effort, default: nil, desc: "Reasoning effort (opencode only; sets reasoningEffort in the model config)"
|
|
387
|
+
option :touch, default: nil, desc: "Comma-separated file globs the lane may touch (records its touch_set for in-bounds + merge checks)"
|
|
211
388
|
|
|
212
|
-
def call(repo:, iteration:, lane:, base: nil, harness: "claude-code", model: nil, effort: nil, **opts)
|
|
389
|
+
def call(repo:, iteration:, lane:, base: nil, harness: "claude-code", model: nil, effort: nil, touch: nil, **opts)
|
|
213
390
|
setup_terminal(**opts.slice(:color, :colors))
|
|
214
391
|
handle_errors do
|
|
215
392
|
render(store.find) do |sp|
|
|
216
393
|
mission = ArchitectMission.new(space: sp)
|
|
394
|
+
touch_set = touch ? touch.split(",").map(&:strip).reject(&:empty?) : nil
|
|
217
395
|
result = mission.worktree_add(repo, iteration, lane, base: base,
|
|
218
|
-
harness: harness, model: model, effort: effort)
|
|
396
|
+
harness: harness, model: model, effort: effort, touch: touch_set)
|
|
219
397
|
terminal.say "Worktree: #{terminal.path(result[:worktree])}"
|
|
220
398
|
terminal.say "Base SHA: #{result[:base_sha]}"
|
|
221
399
|
CLI.record_outcome(Outcome.new(exit_code: 0))
|
|
@@ -376,6 +554,14 @@ SpaceArchitect::CLI::Registry.register "status", SpaceArchitect::CLI::Architect:
|
|
|
376
554
|
SpaceArchitect::CLI::Registry.register "freeze", SpaceArchitect::CLI::Architect::Freeze
|
|
377
555
|
SpaceArchitect::CLI::Registry.register "verify", SpaceArchitect::CLI::Architect::Verify
|
|
378
556
|
SpaceArchitect::CLI::Registry.register "dispatch", SpaceArchitect::CLI::Architect::Dispatch
|
|
557
|
+
SpaceArchitect::CLI::Registry.register "section", SpaceArchitect::CLI::Architect::Section
|
|
558
|
+
SpaceArchitect::CLI::Registry.register "evidence", SpaceArchitect::CLI::Architect::Evidence
|
|
559
|
+
SpaceArchitect::CLI::Registry.register "merge", SpaceArchitect::CLI::Architect::Merge
|
|
560
|
+
SpaceArchitect::CLI::Registry.register "integrate", SpaceArchitect::CLI::Architect::Integrate
|
|
561
|
+
SpaceArchitect::CLI::Registry.register "gate", SpaceArchitect::CLI::Architect::Gate
|
|
562
|
+
SpaceArchitect::CLI::Registry.register "brief" do |b|
|
|
563
|
+
b.register "new", SpaceArchitect::CLI::Architect::Brief::New
|
|
564
|
+
end
|
|
379
565
|
SpaceArchitect::CLI::Registry.register "worktree" do |wt|
|
|
380
566
|
wt.register "add", SpaceArchitect::CLI::Architect::Worktree::Add
|
|
381
567
|
wt.register "remove", SpaceArchitect::CLI::Architect::Worktree::Remove
|
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
> architecture/I<NN>-<iteration>.md — this file only indexes the iterations and carries
|
|
5
5
|
> mission-wide state. Keep it short (~150 lines): the next session must grok it
|
|
6
6
|
> in under a minute. Not in the committed architecture = didn't happen.
|
|
7
|
+
>
|
|
8
|
+
> Durable mission contract: `architecture/BRIEF.md` (numbered §sections, cited as BRIEF §N).
|
|
9
|
+
> Create it with `architect brief new`. Edits to a §section are mission-scope decisions —
|
|
10
|
+
> log them in the Decisions log below, never as silent per-iteration drift.
|
|
7
11
|
|
|
8
12
|
## TL;DR (keep current)
|
|
9
13
|
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# BRIEF — <%= @_title %>
|
|
2
|
+
|
|
3
|
+
> Durable mission contract for the Architect Loop. The numbered §sections below are the
|
|
4
|
+
> stable, cross-iteration address space: every iteration cites them as **BRIEF §N** in its
|
|
5
|
+
> Grounds, Specification, Acceptance Criteria, and Verdict (e.g. `(BRIEF §3.1)`), the way each
|
|
6
|
+
> gate addresses its intent back to one frozen reference. Frozen early; edits are mission-scope
|
|
7
|
+
> decisions logged in ARCHITECT.md's Decisions log, never silent per-iteration drift. Optional —
|
|
8
|
+
> a discovery mission may defer this and cite per-iteration Grounds until the shape stabilizes,
|
|
9
|
+
> then promote the consolidated picture here once.
|
|
10
|
+
|
|
11
|
+
## 1. Goal & non-goals
|
|
12
|
+
|
|
13
|
+
<!-- One paragraph of goal; an explicit non-goals list. §1 is the cardinal-intent address —
|
|
14
|
+
iterations and verdicts cite it for the invariants that must never break. -->
|
|
15
|
+
|
|
16
|
+
## 2. Constraints / frozen stack
|
|
17
|
+
|
|
18
|
+
<!-- Languages, gems/deps and pinned versions, platform constraints. "No new dependencies"
|
|
19
|
+
boundaries in iteration specs cite BRIEF §2. -->
|
|
20
|
+
|
|
21
|
+
## 3. Domain model
|
|
22
|
+
|
|
23
|
+
<!-- The nouns and their relationships; the data/contract shapes that span iterations. -->
|
|
24
|
+
|
|
25
|
+
### 3.1
|
|
26
|
+
|
|
27
|
+
## 4. Layout
|
|
28
|
+
|
|
29
|
+
<!-- Where things live: directories, entry points, the seams iterations build against. -->
|
|
30
|
+
|
|
31
|
+
## 5. Iteration map
|
|
32
|
+
|
|
33
|
+
<!-- The planned iterations, each independently shippable; the Acceptance Criteria are the
|
|
34
|
+
frozen proof. An iteration's Specification cites its row here (e.g. "BRIEF §5 Slice 3"). -->
|
|
35
|
+
|
|
36
|
+
## 6. Cross-iteration risks / PHASE-0 challenge list
|
|
37
|
+
|
|
38
|
+
<!-- Known risks and the things a builder's PHASE 0 should challenge — cited from specs as
|
|
39
|
+
"BRIEF §6". -->
|
|
40
|
+
|
|
41
|
+
## 7. Definition of done (whole mission)
|
|
42
|
+
|
|
43
|
+
<!-- The mission-level DoD. Iteration gates diff against "BRIEF §7 DoD". -->
|
|
@@ -9,21 +9,23 @@
|
|
|
9
9
|
|
|
10
10
|
## Grounds
|
|
11
11
|
|
|
12
|
-
<!-- WHY. Research/
|
|
13
|
-
non-goals, verified facts WITH citation URLs, open questions.
|
|
14
|
-
|
|
12
|
+
<!-- WHY. Research/spec distilled: problem, decision + why, requirements,
|
|
13
|
+
non-goals, verified facts WITH citation URLs, open questions. If
|
|
14
|
+
architecture/BRIEF.md exists, cite **BRIEF §N** instead of restating — shrink this
|
|
15
|
+
to deltas + citations. Optional — delete this section for iterations that needed
|
|
16
|
+
no research. Write + commit: `architect section <%= @_name %> grounds --from <file>`. -->
|
|
15
17
|
|
|
16
18
|
## Specification
|
|
17
19
|
|
|
18
20
|
<!-- WHAT / HOW — the full, self-contained delegation contract.
|
|
19
|
-
|
|
21
|
+
Write + commit: `architect section <%= @_name %> specification --from <file>`. -->
|
|
20
22
|
|
|
21
|
-
- **Objective** — what to build and why
|
|
23
|
+
- **Objective** — what to build and why; cite **BRIEF §N** for durable context (e.g. `(BRIEF §3.1)`).
|
|
22
24
|
- **Output format** — raw tables, numbers, commit SHAs, test output paths.
|
|
23
25
|
- **Tool guidance** — exact verification commands for the target repo; the
|
|
24
26
|
APIs/formats/versions to verify against live dependencies before writing code.
|
|
25
27
|
- **Boundaries** — may-touch / must-not-touch / out-of-scope; no placeholders;
|
|
26
|
-
no refactors beyond the task.
|
|
28
|
+
no refactors beyond the task. (Frozen stack: cite `BRIEF §2`.)
|
|
27
29
|
- **Lane plan** — 1–4 lanes, each declaring: target repo `repos/<repo>`,
|
|
28
30
|
file-touch set (overlap-checked), objective, output format, boundaries.
|
|
29
31
|
- **Effort** — `think hard` … `ultrathink` per lane, with one line of why.
|
|
@@ -34,33 +36,39 @@ Commit: "<%= @_ordinal %>: specification". -->
|
|
|
34
36
|
commits this file and records its SHA as freeze_sha. Read-only afterward — any
|
|
35
37
|
change to Grounds/Specification/Acceptance Criteria = automatic iteration FAIL. -->
|
|
36
38
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
39
|
+
> Gate-pass is necessary, not sufficient: the architect also reads the diff against the
|
|
40
|
+
> cited **BRIEF §sections** intent and the §1 cardinal invariant before the verdict.
|
|
41
|
+
|
|
42
|
+
| AC# | Command | Threshold | Brief § |
|
|
43
|
+
|-----|---------|-----------|---------|
|
|
44
|
+
| | | | |
|
|
40
45
|
|
|
41
46
|
## Builder Prompt
|
|
42
47
|
|
|
43
48
|
<!-- The exact lane-prompt(s) dispatched, recorded as provenance. One ###
|
|
44
49
|
subsection per lane. A copy is written to build/<%= @_ordinal %>-<%= @_name %>-<lane>/prompt.md
|
|
45
|
-
for stdin dispatch.
|
|
50
|
+
for stdin dispatch. Record + commit:
|
|
51
|
+
`architect section <%= @_name %> prompt --append --lane <lane> --from build/<%= @_ordinal %>-<%= @_name %>-<lane>/prompt.md`. -->
|
|
46
52
|
|
|
47
53
|
## Builder Report
|
|
48
54
|
|
|
49
55
|
<!-- RAW EVIDENCE ONLY — tables, numbers, command output. The builder writes this
|
|
50
56
|
to build/<%= @_ordinal %>-<%= @_name %>-<lane>/report.md; the architect transcribes it here
|
|
51
|
-
VERBATIM
|
|
52
|
-
|
|
57
|
+
VERBATIM with `architect evidence <%= @_name %> --lane <lane>` (byte-for-byte, no
|
|
58
|
+
interpretation). One ### subsection per lane; includes the builder's PHASE 0
|
|
59
|
+
disagreements and its STATUS line. -->
|
|
53
60
|
|
|
54
61
|
## Verdict
|
|
55
62
|
|
|
56
63
|
<!-- ARCHITECT JUDGMENT, written in a LATER session than the dispatch.
|
|
57
|
-
|
|
64
|
+
Write + commit: `architect section <%= @_name %> verdict --from <file>`. -->
|
|
58
65
|
|
|
59
|
-
- **Disagreement rulings** — each PHASE 0 disagreement: ACCEPT / REJECT / MODIFY + why
|
|
66
|
+
- **Disagreement rulings** — each PHASE 0 disagreement: ACCEPT / REJECT / MODIFY + why; cite the
|
|
67
|
+
BRIEF §section in tension beside file:line.
|
|
60
68
|
- **Acceptance Criteria integrity** — frozen sections unchanged since freeze (`architect verify <%= @_name %>`)?
|
|
61
69
|
|
|
62
|
-
| AC# | Raw result | Verdict |
|
|
63
|
-
|
|
64
|
-
| | | PASS/FAIL/INVALID |
|
|
70
|
+
| AC# | Raw result | Brief § | Verdict |
|
|
71
|
+
|-----|------------|---------|---------|
|
|
72
|
+
| | | | PASS/FAIL/INVALID |
|
|
65
73
|
|
|
66
|
-
- **Iteration** — KILL / CONTINUE + the single decisive reason.
|
|
74
|
+
- **Iteration** — KILL / CONTINUE + the single decisive reason (e.g. "diff vs BRIEF §1/§3.3 faithful — CONTINUE").
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: space-architect
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Eric Jacobs
|
|
@@ -241,6 +241,7 @@ files:
|
|
|
241
241
|
- lib/space_architect/space_store.rb
|
|
242
242
|
- lib/space_architect/state.rb
|
|
243
243
|
- lib/space_architect/templates/architect.md.erb
|
|
244
|
+
- lib/space_architect/templates/brief.md.erb
|
|
244
245
|
- lib/space_architect/templates/iteration.md.erb
|
|
245
246
|
- lib/space_architect/terminal.rb
|
|
246
247
|
- lib/space_architect/version.rb
|