space-architect 4.0.0 → 5.0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bb8f902d475bcc8a29e56740ce0c20328935fe05f9d919d38934b2cffd12e966
4
- data.tar.gz: e31c5b6f8ad98219c9c4ed22225666900bc7aaf8f43bd929ef32e2d2b5aacdf2
3
+ metadata.gz: 6846eab011f92f0b54a7bb886d7b97cf8c727beb3fc6d279a2db603062645520
4
+ data.tar.gz: 0bc8f0b22f785fd2d3ed77419a1ad3b6058e4b76a6aaee41b7a66793b865702a
5
5
  SHA512:
6
- metadata.gz: 5cbb47cbacf2231ed09eb74bfb11b80a1a6076bfde1427fe76a19b3581f3b3a8e38db4a6269307e339a022902723d73b1d40dd4b001acd6ffaab624871249d70
7
- data.tar.gz: 171913d786d1e166a279f72b184ec7987785d561ec795bfe161db9341502210f6d97166c3eb8b7a097b59adba7e435a575c78ca66488ca482312b4bc7cca7249
6
+ metadata.gz: 77216fd23779d4ee5ae16f3c141fc9f7670b3efc3c8b7fe31292c31284be0903616505c4199fbe4e5d7b12988d0638e7a31cba600a4625caad2b939255fc9370
7
+ data.tar.gz: ca1fb9e9bd1c20c6ef201d60f763cdcb87b18b9c289169ff601be6c858b0558f283986b2b24bb82cf73b4b1001c3ce0a9955bd02675557548fcc2e930aea27d2
data/CHANGELOG.md CHANGED
@@ -5,6 +5,92 @@ All notable changes to this project are documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [5.0.0] - 2026-07-06
9
+
10
+ ### Added
11
+
12
+ - **`architect dispatch --prompt <file>`** — the lane prompt is authored anywhere
13
+ (a fresh scratch file) and dispatch copies it byte-for-byte to the canonical
14
+ `build/<id>-<lane>/prompt.md` before launch, announcing the copy. The CLI owns
15
+ the canonical path; the architect never blind-writes it (#48, #42).
16
+ - **`architect brief new --from <file>` / `--stdin`** — write the authored brief
17
+ in one step instead of filling a pre-seeded template. Bare `brief new` still
18
+ scaffolds the placeholder, and now says so
19
+ (`Brief ready: … (template — Read it before editing)`).
20
+ - **Flexible commit messages on every committing loop command** — `init`, `new`,
21
+ `freeze`, `brief new`, `section`, `verdict`, `evidence`, `merge`, and
22
+ `integrate` all take `-m`/`--message` and `--message-from <file>`. The first
23
+ line completes the subject after a short canonical prefix (`I01 spec: <your
24
+ subject>`, `lane <lane>: <your subject>`); remaining lines become the commit
25
+ body. Without the flags, the canonical messages are unchanged. The space's git
26
+ log is the loop's durable memory — detailed bodies are the point.
27
+
28
+ ### Changed
29
+
30
+ - **`worktree add`/`provision` no longer pre-seed `prompt.md` with a placeholder
31
+ stub.** The unannounced stub made the architect's first `Write` trip the
32
+ harness read-before-write guard on every single lane (a failed-write → read →
33
+ rewrite round trip; 40 occurrences mined from real transcripts). Dispatch
34
+ still refuses a missing, empty, or legacy-stub prompt (#48, #42).
35
+ - **`architect merge --message` semantics** — the message now composes with the
36
+ canonical `lane <lane>:` prefix (subject + body) instead of replacing the
37
+ whole message, matching every other committing command.
38
+ - **Skill prose** — the architect authors all content (lane prompts, brief, PR
39
+ bodies) in fresh timestamped scratch files and hands them to the CLI via
40
+ `--from`/`--prompt`; PR bodies land at
41
+ `build/land/<repo>-pr-body-<yyyymmdd-hhmm>.md`; committing commands should
42
+ carry detailed `--message-from` bodies.
43
+
44
+ ## [4.0.0] - 2026-07-05
45
+
46
+ Backfilled — the bump shipped without an entry. Five iterations run through the
47
+ Architect Loop against `space-architect`'s own live-loop papercuts (#46), plus
48
+ the `dispatched_at` producer (#45) and a test-suite hygiene pass (#32).
49
+
50
+ ### Added
51
+
52
+ - **`architect provision <iteration> [--base <ref>] [--lane <name>]`** — materializes
53
+ every declared lane's worktree + `lane/<id>-<lane>` branch in one shot from the
54
+ frozen lane plan (idempotent; base resolves `--base`, else `project/<slug>` HEAD,
55
+ else the repo's default branch). `dispatch`/`integrate`/`gate`/`verify`
56
+ auto-materialize a missing worktree from the frozen declaration, so the flow
57
+ can't dead-end (#26).
58
+ - **Fenced ` ```lanes ` block in the Specification** — `name`, `repo`, `touch`
59
+ globs, parsed at **freeze** into `space.yaml` lane entries, making the lane plan
60
+ (including the out-of-bounds touch-set contract) part of the frozen spec (#26).
61
+ - **`space.yaml` schema v2** — the never-bumped `version` key now means something:
62
+ canonicalize on save, read + self-heal the two known v1 variants (v1a
63
+ `architect:`, v1b `project:`+`version: 1`) on load; a both-keys conflict refuses
64
+ and names both blocks rather than silently picking one (#33).
65
+ - **`dispatched_at` on lane entries** — every dispatch stamps an ISO 8601 launch
66
+ time after preflight, before the run, on both foreground and detached paths
67
+ (#18, producer landed via #45).
68
+ - **Self-verifying dispatch liveness** — a transient fiber inside dispatch checks
69
+ the launched run matches intent (model, growth of `run.jsonl`), retiring the
70
+ manual "canary" ceremony (#43, #44).
71
+ - **`architect help` grouped by loop phase** — Spec / Build / Judge / Land /
72
+ Project, commands in loop order, with an embedded loop-status block when run
73
+ inside a project space; `space status` reports on a bare call and still sets on
74
+ `space status <value>`.
75
+
76
+ ### Changed
77
+
78
+ - **Lane declaration is single-source-of-truth** — lanes were declared twice
79
+ (Specification prose + `worktree add` flags) with the touch-set divorced from
80
+ the frozen spec; the ` ```lanes ` block at freeze is now the one declaration.
81
+ The single-lane "dispatch in the repo checkout" fast path is removed (it never
82
+ worked with `merge_lane!`); manual `worktree add` remains as the internal
83
+ primitive `provision` wraps (#26).
84
+ - **Skill prose** (`SKILL.md` §4–§6, `dispatch.md`) rewritten to the
85
+ declare → freeze → provision → dispatch lane lifecycle.
86
+
87
+ ### Fixed
88
+
89
+ - **`architect integrate <it> --teardown`** (no `--lanes`) backtraced; teardown-only
90
+ mode now deletes per-lane branches and worktrees, never `project/<slug>` or
91
+ `main` (#30).
92
+ - **Test suite hygiene** — zero warnings, zero stray output, 58.8s → 49.1s (#32).
93
+
8
94
  ## [3.0.0] - 2026-07-01
9
95
 
10
96
  ### Added
data/README.md CHANGED
@@ -271,8 +271,12 @@ build/
271
271
  ```
272
272
 
273
273
  **Iteration file anatomy** — one file, grown section by section. You author the
274
- *content*; the CLI owns the *persistence* (each command writes the section,
275
- commits it with the canonical message, and prints back what changed):
274
+ *content* in fresh scratch files; the CLI owns the *persistence* (each command
275
+ writes the section, commits it, and prints back what changed). Every committing
276
+ command takes `-m`/`--message` and `--message-from <file>` — your first line
277
+ completes the subject after a short canonical prefix (`I01 spec: <subject>`),
278
+ the rest becomes the body. The space's git log is the loop's durable memory:
279
+ write detailed messages.
276
280
 
277
281
  | Section | Holds | How you persist it |
278
282
  |---------|-------|--------------------|
@@ -299,12 +303,12 @@ inside a lane worktree are never grounded.
299
303
 
300
304
  ```sh
301
305
  architect init # scaffold ARCHITECT.md + the space.yaml project: block + SessionStart hook
302
- architect brief new # scaffold the durable project BRIEF.md
306
+ architect brief new --from <f> # write the durable project BRIEF.md (authored in a scratch file)
303
307
  architect new <iteration> # scaffold architecture/I<NN>-<iteration>.md
304
- architect section <it> <section> --from <f> # write + commit a section
308
+ architect section <it> <section> --from <f> # write + commit a section (add -m/--message-from for a detailed commit)
305
309
  architect freeze <iteration> # freeze the Acceptance Criteria ❄️
306
310
  architect worktree add <repo> <it> <lane> # isolated worktree per lane (2–4 lanes)
307
- architect dispatch <it> <lane> # dispatch a builder (add --detach to survive long runs)
311
+ architect dispatch <it> <lane> --prompt <f> # copy the lane prompt in + dispatch a builder (--detach to survive long runs)
308
312
  architect verify <iteration> # post-flight mechanical checks (reports only)
309
313
  architect evidence <it> --lane <lane> # transcribe the builder's report verbatim
310
314
  architect gate <iteration> # run the frozen gate commands, stream raw output
@@ -326,9 +330,9 @@ A typical session:
326
330
  ```sh
327
331
  architect init # first time
328
332
  architect new my-feature # scaffold I01-my-feature.md
329
- architect section my-feature specification --from spec.md
333
+ architect section my-feature specification --from spec.md -m "pull-based dispatcher seam"
330
334
  architect freeze my-feature # lock it ❄️
331
- architect dispatch my-feature lane-a --detach # send a builder; poll the report
335
+ architect dispatch my-feature lane-a --prompt tmp/prompts/I01-lane-a.md --detach # send a builder; poll the report
332
336
  architect verify my-feature # mechanical post-flight checks
333
337
  architect evidence my-feature --lane lane-a # transcribe raw evidence
334
338
  architect gate my-feature # run the frozen gates yourself
@@ -25,10 +25,10 @@ module Space::Architect
25
25
  # command (`architect evidence`) because it is transcribed verbatim from scratch.
26
26
  # `frozen: true` sections live above the freeze boundary and are refused once frozen.
27
27
  SECTIONS = {
28
- "grounds" => { heading: "## Grounds", message: "grounds", frozen: true },
29
- "specification" => { heading: "## Specification", message: "specification", frozen: true },
30
- "prompt" => { heading: "## Builder Prompt", message: "dispatched", frozen: false },
31
- "verdict" => { heading: "## Verdict", message: "verdict", frozen: false }
28
+ "grounds" => { heading: "## Grounds", message: "grounds", prefix: "grounds", frozen: true },
29
+ "specification" => { heading: "## Specification", message: "specification", prefix: "spec", frozen: true },
30
+ "prompt" => { heading: "## Builder Prompt", message: "dispatched", prefix: "prompt", frozen: false },
31
+ "verdict" => { heading: "## Verdict", message: "verdict", prefix: "verdict", frozen: false }
32
32
  }.freeze
33
33
 
34
34
  # The fixed top-level section headings. Section boundaries are detected against
@@ -42,7 +42,10 @@ module Space::Architect
42
42
  # Hard per-gate timeout. Generous relative to the full suite (~55s).
43
43
  DEFAULT_GATE_TIMEOUT = 900
44
44
 
45
- # Sentinel written to prompt.md by worktree_add. dispatch refuses to launch on this content.
45
+ # Legacy sentinel: worktree_add used to seed prompt.md with this placeholder
46
+ # (dropped — the blind-overwrite tripped harness read-before-write guards, #48).
47
+ # dispatch still refuses to launch on this content, so stubs in old spaces
48
+ # can't reach a builder.
46
49
  PROMPT_STUB = "<!-- ARCHITECT: write this lane's builder prompt here, then dispatch. -->"
47
50
 
48
51
  # Inlined settings.json template for `architect init`. Registers a SessionStart
@@ -74,7 +77,7 @@ module Space::Architect
74
77
  @space = space
75
78
  end
76
79
 
77
- def init!
80
+ def init!(message: nil)
78
81
  handoff_path = space.path.join("architecture", "ARCHITECT.md")
79
82
  settings_path = space.path.join(".claude", "settings.json")
80
83
  to_add = []
@@ -97,15 +100,15 @@ module Space::Architect
97
100
 
98
101
  if to_add.any?
99
102
  git_run("-C", space.path.to_s, "add", *to_add)
100
- msg = to_add.include?("architecture/ARCHITECT.md") ? "Initialize architect project" : "Add architect settings"
101
- git_run("-C", space.path.to_s, "commit", "-m", msg)
103
+ default = to_add.include?("architecture/ARCHITECT.md") ? "Initialize architect project" : "Add architect settings"
104
+ git_run("-C", space.path.to_s, "commit", "-m", compose_message("init:", default, message))
102
105
  end
103
106
 
104
107
  handoff_path
105
108
  end
106
109
 
107
110
  # Allocate the next ordinal and scaffold architecture/I<NN>-<iteration>.md.
108
- def new_iteration!(name)
111
+ def new_iteration!(name, message: nil)
109
112
  block = space.data["project"] || {}
110
113
  iterations = block["iterations"] || []
111
114
  if iterations.any? { |s| s["name"] == name }
@@ -133,7 +136,8 @@ module Space::Architect
133
136
  end
134
137
 
135
138
  git_run("-C", space.path.to_s, "add", rel, Space::Core::Space::METADATA_FILE)
136
- git_run("-C", space.path.to_s, "commit", "-m", "I#{nn}: scaffold #{name}")
139
+ git_run("-C", space.path.to_s, "commit", "-m",
140
+ compose_message("I#{nn} scaffold:", "I#{nn}: scaffold #{name}", message))
137
141
 
138
142
  path
139
143
  end
@@ -154,7 +158,7 @@ module Space::Architect
154
158
  # Freeze the iteration: the iteration file must carry a "## Acceptance Criteria" section. Commits
155
159
  # any pending changes to the iteration file and records HEAD as freeze_sha. If
156
160
  # already frozen, refuses when the frozen region has changed since.
157
- def freeze!(iteration, warnings: nil)
161
+ def freeze!(iteration, warnings: nil, message: nil)
158
162
  entry = slice_entry(iteration)
159
163
  rel = entry["file"]
160
164
  path = space.path.join(rel)
@@ -180,7 +184,8 @@ module Space::Architect
180
184
  files = [rel]
181
185
  files << "architecture/ARCHITECT.md" if space.path.join("architecture", "ARCHITECT.md").exist?
182
186
  nn = format("%02d", entry["ordinal"] || 0)
183
- git_capture("-C", space.path.to_s, "commit", "-m", "I#{nn}: acceptance criteria (freeze)", "--", *files)
187
+ git_capture("-C", space.path.to_s, "commit", "-m",
188
+ compose_message("I#{nn} freeze:", "I#{nn}: acceptance criteria (freeze)", message), "--", *files)
184
189
 
185
190
  sha, = git_capture("-C", space.path.to_s, "rev-parse", "HEAD")
186
191
  sha = sha.strip
@@ -208,17 +213,20 @@ module Space::Architect
208
213
 
209
214
  # Scaffold the durable, section-numbered project brief at architecture/BRIEF.md
210
215
  # and commit it. The brief is the stable cross-iteration address space iterations
211
- # cite as "BRIEF §N"; it lives outside the per-iteration freeze region.
212
- def brief_new!(force: false)
216
+ # cite as "BRIEF §N"; it lives outside the per-iteration freeze region. With
217
+ # content, writes the authored brief instead of the placeholder template.
218
+ def brief_new!(force: false, content: nil, message: nil)
213
219
  brief_path = space.path.join("architecture", "BRIEF.md")
214
220
  if brief_path.exist? && !force
215
221
  raise Space::Core::Error, "architecture/BRIEF.md already exists — edit it directly (idempotent guard), or pass --force to overwrite"
216
222
  end
217
223
 
218
224
  FileUtils.mkdir_p(brief_path.dirname)
219
- brief_path.write(render_brief)
225
+ brief_path.write(content || render_brief)
220
226
  git_run("-C", space.path.to_s, "add", "architecture/BRIEF.md")
221
- git_run("-C", space.path.to_s, "commit", "-m", "Add project brief") if staged_changes?
227
+ if staged_changes?
228
+ git_run("-C", space.path.to_s, "commit", "-m", compose_message("brief:", "Add project brief", message))
229
+ end
222
230
  brief_path
223
231
  end
224
232
 
@@ -226,7 +234,7 @@ module Space::Architect
226
234
  # per-section message, in one call. Refuses to write a frozen section
227
235
  # (Grounds/Specification) once the iteration is frozen. Acceptance Criteria is
228
236
  # NOT writable here (use freeze); Builder Report is not here (use evidence).
229
- def write_section!(iteration, section, body:, append: false, lane: nil)
237
+ def write_section!(iteration, section, body:, append: false, lane: nil, message: nil)
230
238
  spec = SECTIONS[section]
231
239
  unless spec
232
240
  raise Space::Core::Error,
@@ -249,7 +257,8 @@ module Space::Architect
249
257
  path.write(replace_section_body(path.read, spec[:heading], block, append: append))
250
258
 
251
259
  nn = format("%02d", entry["ordinal"] || 0)
252
- _o, _e, cst = git_capture("-C", space.path.to_s, "commit", "-m", "I#{nn}: #{spec[:message]}", "--", rel)
260
+ _o, _e, cst = git_capture("-C", space.path.to_s, "commit", "-m",
261
+ compose_message("I#{nn} #{spec[:prefix]}:", "I#{nn}: #{spec[:message]}", message), "--", rel)
253
262
  committed = cst.success?
254
263
  show_out, = git_capture("-C", space.path.to_s, "show", "--stat", "--format=%H", "HEAD")
255
264
  show_lines = show_out.to_s.lines
@@ -260,7 +269,7 @@ module Space::Architect
260
269
 
261
270
  # Write the ## Verdict prose AND record the decision to space.yaml in one commit.
262
271
  # decision must be "continue" or "kill".
263
- def record_verdict!(iteration, decision:, body:)
272
+ def record_verdict!(iteration, decision:, body:, message: nil)
264
273
  unless %w[continue kill].include?(decision)
265
274
  raise Space::Core::Error,
266
275
  "Invalid verdict decision '#{decision}' — must be one of: continue, kill"
@@ -279,7 +288,8 @@ module Space::Architect
279
288
  end
280
289
 
281
290
  nn = format("%02d", entry["ordinal"] || 0)
282
- git_run("-C", space.path.to_s, "commit", "-m", "I#{nn}: verdict", "--", rel, Space::Core::Space::METADATA_FILE)
291
+ git_run("-C", space.path.to_s, "commit", "-m",
292
+ compose_message("I#{nn} verdict:", "I#{nn}: verdict", message), "--", rel, Space::Core::Space::METADATA_FILE)
283
293
 
284
294
  head, = git_capture("-C", space.path.to_s, "rev-parse", "HEAD")
285
295
  { decision: decision, sha: head.strip }
@@ -287,7 +297,7 @@ module Space::Architect
287
297
 
288
298
  # Transcribe a lane's scratch report (build/<id>[-<lane>]/report.md) VERBATIM into
289
299
  # the Builder Report section and commit. Byte-for-byte: no summarization, no judgment.
290
- def transcribe_evidence!(iteration, lane: nil)
300
+ def transcribe_evidence!(iteration, lane: nil, message: nil)
291
301
  entry = slice_entry(iteration)
292
302
  rel = entry["file"]
293
303
  path = space.path.join(rel)
@@ -303,7 +313,8 @@ module Space::Architect
303
313
  path.write(replace_section_body(path.read, "## Builder Report", block, append: !lane.nil?))
304
314
 
305
315
  nn = format("%02d", entry["ordinal"] || 0)
306
- git_capture("-C", space.path.to_s, "commit", "-m", "I#{nn}: evidence", "--", rel)
316
+ git_capture("-C", space.path.to_s, "commit", "-m",
317
+ compose_message("I#{nn} evidence:", "I#{nn}: evidence", message), "--", rel)
307
318
  head, = git_capture("-C", space.path.to_s, "rev-parse", "HEAD")
308
319
 
309
320
  status_line = raw.lines.reverse_each.find { |l| l.strip.start_with?("STATUS:") }&.strip
@@ -358,7 +369,8 @@ module Space::Architect
358
369
  raise Space::Core::Error, "Lane '#{lane}' worktree has no changes to integrate." if status_out.strip.empty?
359
370
 
360
371
  git_run("-C", wt_path.to_s, "add", "-A")
361
- git_run("-C", wt_path.to_s, "commit", "-m", message || "lane #{lane}: integrate")
372
+ git_run("-C", wt_path.to_s, "commit", "-m",
373
+ compose_message("lane #{lane}:", "lane #{lane}: integrate", message))
362
374
  integrate_sha_raw, = git_capture("-C", wt_path.to_s, "rev-parse", "HEAD")
363
375
  integrate_sha = integrate_sha_raw.strip
364
376
 
@@ -402,14 +414,14 @@ module Space::Architect
402
414
  # first conflict (a disjointness defect). Never decides which lanes pass. With no
403
415
  # lanes and teardown: true, tears down every lane recorded for the iteration instead
404
416
  # (the second, teardown-only call in the loop's integrate-then-teardown rhythm).
405
- def integrate!(iteration, lanes: nil, teardown: false)
417
+ def integrate!(iteration, lanes: nil, teardown: false, message: nil)
406
418
  lanes = Array(lanes)
407
419
  return teardown_lanes!(iteration, slice_entry(iteration)["lanes"] || []) if lanes.empty? && teardown
408
420
  raise Space::Core::Error, "No lanes given to integrate" if lanes.empty?
409
421
 
410
422
  merged = []
411
423
  lanes.each do |lane|
412
- merged << merge_lane!(iteration, lane)
424
+ merged << merge_lane!(iteration, lane, message: message)
413
425
  rescue Space::Core::Error => e
414
426
  done = merged.map { |m| m[:lane] }.join(", ")
415
427
  raise Space::Core::Error, "Integrated #{done.empty? ? "(none)" : done} then stopped at '#{lane}': #{e.message}"
@@ -572,11 +584,6 @@ module Space::Architect
572
584
  end
573
585
  end
574
586
 
575
- # Seed prompt.md with a placeholder stub so the architect has a place to write the prompt.
576
- # Never overwrite an existing file (real prompt or stub from a prior run).
577
- prompt_path = build_dir.join("prompt.md")
578
- prompt_path.write("#{PROMPT_STUB}\n") unless prompt_path.exist?
579
-
580
587
  new_fields = {
581
588
  "name" => lane,
582
589
  "repo" => repo,
@@ -755,7 +762,7 @@ module Space::Architect
755
762
  def dispatch(iteration, lane, model: nil, max_turns: 200,
756
763
  claude_bin: nil, harness: nil, opencode_bin: nil, effort: nil, detach: false,
757
764
  push_url: nil, push_token: nil, push_host: nil, run_creator: nil,
758
- push_client: nil, timeout: nil, now: Time.now)
765
+ push_client: nil, timeout: nil, prompt: nil, now: Time.now)
759
766
  raise Space::Core::Error, "Specify --push-host or --push-url, not both" if push_host && push_url
760
767
  raise Space::Core::Error, "--push-host requires --push-token" if push_host && !push_token
761
768
  raise Space::Core::Error, "--detach cannot be combined with --push-url or --push-host" \
@@ -781,6 +788,15 @@ module Space::Architect
781
788
  prompt_path = build_dir.join("prompt.md")
782
789
  run_log_path = build_dir.join("run.jsonl")
783
790
  report_path = build_dir.join("report.md")
791
+
792
+ # --prompt: the caller authors the lane prompt anywhere (a fresh scratch file)
793
+ # and the CLI owns the canonical copy — byte-for-byte, like variant_add.
794
+ if prompt
795
+ src = Pathname.new(prompt)
796
+ raise Space::Core::Error, "prompt file not found: #{src}" unless src.exist?
797
+ File.open(prompt_path, "wb") { |f| f.write(File.binread(src)) }
798
+ end
799
+
784
800
  raise Space::Core::Error, "prompt.md not found: #{prompt_path}" unless prompt_path.exist?
785
801
 
786
802
  prompt_content = prompt_path.read.strip
@@ -811,7 +827,9 @@ module Space::Architect
811
827
  run_log_path: run_log_path,
812
828
  chdir: wt_path
813
829
  )
814
- { pid: pid, run_log: run_log_path, report: report_path, worktree: wt_path }
830
+ result = { pid: pid, run_log: run_log_path, report: report_path, worktree: wt_path }
831
+ result[:prompt_copied] = prompt_path if prompt
832
+ result
815
833
  else
816
834
  created_run_id = nil
817
835
  if push_host
@@ -830,6 +848,7 @@ module Space::Architect
830
848
  exit_code = harness_obj.run(**run_kwargs)
831
849
 
832
850
  result = { exit_code: exit_code, run_log: run_log_path, report: report_path, worktree: wt_path }
851
+ result[:prompt_copied] = prompt_path if prompt
833
852
  result[:timed_out] = true if exit_code == Harness::ClaudeCodeHarness::TIMEOUT_EXIT_CODE
834
853
  result[:created_run_id] = created_run_id if created_run_id
835
854
  result[:push_url] = push_url if push_url
@@ -841,6 +860,20 @@ module Space::Architect
841
860
 
842
861
  attr_reader :space
843
862
 
863
+ # Compose a commit message. Without a custom message, the canonical default
864
+ # (unchanged). With one, a short canonical prefix keeps the loop's commit
865
+ # taxonomy grep-able while the author's first line owns the subject; any
866
+ # remaining lines become the commit body — the space's git log is the loop's
867
+ # durable memory, so callers are encouraged to write detailed bodies.
868
+ def compose_message(prefix, default, message)
869
+ return default if message.nil? || message.strip.empty?
870
+
871
+ subject, _, body = message.strip.partition("\n")
872
+ composed = "#{prefix} #{subject.strip}"
873
+ body = body.strip
874
+ body.empty? ? composed : "#{composed}\n\n#{body}"
875
+ end
876
+
844
877
  # Remove each lane's worktree and safe-delete (`-d`) its lane branch. Accepts
845
878
  # either merge_lane! results (symbol keys) or recorded lane entries (string
846
879
  # keys) — both carry a lane name and a repo.
@@ -15,6 +15,36 @@ module Space::Architect
15
15
 
16
16
  @phase = [order, label]
17
17
  end
18
+
19
+ # Declares -m/--message and --message-from on a committing command. Every
20
+ # loop command that commits takes both: the space's git log is the loop's
21
+ # durable memory, so detailed messages are encouraged everywhere.
22
+ def self.commit_message_options
23
+ option :message, aliases: ["-m"], default: nil,
24
+ desc: "Commit message: first line completes the subject after the canonical prefix, the rest becomes the body"
25
+ option :message_from, default: nil,
26
+ desc: "Read the commit message from this file (subject line + detailed body)"
27
+ end
28
+
29
+ private
30
+
31
+ # Authored-content intake shared by section/verdict/brief: a file, an
32
+ # inline flag, or stdin — canonical files are only ever written by the CLI.
33
+ def read_body(from: nil, body: nil, stdin: false, what: "section body")
34
+ return File.read(from) if from
35
+ return body if body
36
+ return $stdin.read if stdin
37
+
38
+ raise Space::Core::Error, "provide the #{what} via --from <file>, --body <text>, or --stdin"
39
+ end
40
+
41
+ # Commit-message intake for commit_message_options: --message-from wins
42
+ # over -m/--message; nil means the command's canonical default message.
43
+ def read_commit_message(message: nil, message_from: nil)
44
+ return File.read(message_from) if message_from
45
+
46
+ message
47
+ end
18
48
  end
19
49
 
20
50
  module Architect
@@ -22,13 +52,14 @@ module Space::Architect
22
52
  desc "Scaffold (or top up) the architect project: ARCHITECT.md, space.yaml project block, SessionStart hook"
23
53
  phase 50, "Project"
24
54
  argument :space, required: false, desc: "Space identifier (default: $PWD)"
55
+ commit_message_options
25
56
 
26
- def call(space: nil, **opts)
57
+ def call(space: nil, message: nil, message_from: nil, **opts)
27
58
  setup_terminal(**opts.slice(:color, :colors))
28
59
  handle_errors do
29
60
  render(store.find(space)) do |sp|
30
61
  project = ArchitectProject.new(space: sp)
31
- path = project.init!
62
+ path = project.init!(message: read_commit_message(message: message, message_from: message_from))
32
63
  terminal.say "Project ready: #{terminal.path(path)}"
33
64
  CLI.record_outcome(Outcome.new(exit_code: 0))
34
65
  end
@@ -74,13 +105,15 @@ module Space::Architect
74
105
  phase 10, "Spec"
75
106
  argument :iteration, required: true, desc: "Iteration name (kebab-case)"
76
107
  argument :space, required: false, desc: "Space identifier (default: $PWD)"
108
+ commit_message_options
77
109
 
78
- def call(iteration:, space: nil, **opts)
110
+ def call(iteration:, space: nil, message: nil, message_from: nil, **opts)
79
111
  setup_terminal(**opts.slice(:color, :colors))
80
112
  handle_errors do
81
113
  render(store.find(space)) do |sp|
82
114
  project = ArchitectProject.new(space: sp)
83
- path = project.new_iteration!(iteration)
115
+ path = project.new_iteration!(iteration,
116
+ message: read_commit_message(message: message, message_from: message_from))
84
117
  terminal.say "Iteration scaffolded: #{terminal.path(path)}"
85
118
  CLI.record_outcome(Outcome.new(exit_code: 0))
86
119
  end
@@ -146,14 +179,16 @@ module Space::Architect
146
179
  phase 12, "Spec"
147
180
  argument :iteration, required: true, desc: "Iteration name"
148
181
  argument :space, required: false, desc: "Space identifier (default: $PWD)"
182
+ commit_message_options
149
183
 
150
- def call(iteration:, space: nil, **opts)
184
+ def call(iteration:, space: nil, message: nil, message_from: nil, **opts)
151
185
  setup_terminal(**opts.slice(:color, :colors))
152
186
  handle_errors do
153
187
  render(store.find(space)) do |sp|
154
188
  project = ArchitectProject.new(space: sp)
155
189
  warnings = []
156
- sha = project.freeze!(iteration, warnings: warnings)
190
+ sha = project.freeze!(iteration, warnings: warnings,
191
+ message: read_commit_message(message: message, message_from: message_from))
157
192
  terminal.say "Frozen #{iteration} at #{sha}"
158
193
  warnings.each { |w| terminal.say "Warning: #{w}" }
159
194
  ac = project.acceptance_criteria(iteration)
@@ -222,6 +257,7 @@ module Space::Architect
222
257
  argument :iteration, required: true, desc: "Iteration name"
223
258
  argument :lane, required: true, desc: "Lane name"
224
259
  argument :space, required: false, desc: "Space identifier (default: $PWD)"
260
+ option :prompt, default: nil, desc: "Read the lane prompt from this file (copied byte-for-byte to build/<id>-<lane>/prompt.md)"
225
261
  option :model, default: nil, desc: "Builder model to pin (default: the lane's model, else the reference default claude-sonnet-4-6). Any provider/tier; pin a full id, not a floating alias"
226
262
  option :max_turns, default: "200", desc: "Max turns for the builder"
227
263
  option :harness, default: nil, desc: "Harness override (claude-code, opencode)"
@@ -232,7 +268,7 @@ module Space::Architect
232
268
  option :push_token, default: nil, desc: "Bearer token for push endpoint authorization"
233
269
  option :push_host, default: nil, desc: "Base URL of the ingest server; the CLI creates a run via POST <host>/runs and streams to /runs/<id>/ingest (requires --push-token)"
234
270
 
235
- def call(iteration:, lane:, space: nil, model: nil,
271
+ def call(iteration:, lane:, space: nil, prompt: nil, model: nil,
236
272
  max_turns: "200", harness: nil, effort: nil, detach: false,
237
273
  timeout: "14400", push_url: nil, push_token: nil, push_host: nil, **opts)
238
274
  setup_terminal(**opts.slice(:color, :colors))
@@ -240,6 +276,7 @@ module Space::Architect
240
276
  render(store.find(space)) do |sp|
241
277
  project = ArchitectProject.new(space: sp)
242
278
  kwargs = { max_turns: max_turns.to_i, detach: detach }
279
+ kwargs[:prompt] = prompt if prompt
243
280
  kwargs[:model] = model if model
244
281
  kwargs[:harness] = harness if harness
245
282
  kwargs[:effort] = effort if effort
@@ -248,6 +285,7 @@ module Space::Architect
248
285
  kwargs[:push_token] = push_token if push_token
249
286
  kwargs[:push_host] = push_host if push_host
250
287
  res = project.dispatch(iteration, lane, **kwargs)
288
+ terminal.say "Prompt: #{prompt} → #{terminal.path(res[:prompt_copied])}" if res[:prompt_copied]
251
289
  if detach
252
290
  terminal.say "PID: #{res[:pid]}"
253
291
  terminal.say "Run log: #{terminal.path(res[:run_log])}"
@@ -310,14 +348,17 @@ module Space::Architect
310
348
  option :stdin, type: :boolean, default: false, desc: "Read the section body from stdin"
311
349
  option :append, type: :boolean, default: false, desc: "Append a ### <lane> subsection instead of replacing"
312
350
  option :lane, default: nil, desc: "Lane name for an appended ### subsection"
351
+ commit_message_options
313
352
 
314
- def call(iteration:, section:, space: nil, from: nil, body: nil, stdin: false, append: false, lane: nil, **opts)
353
+ def call(iteration:, section:, space: nil, from: nil, body: nil, stdin: false, append: false, lane: nil,
354
+ message: nil, message_from: nil, **opts)
315
355
  setup_terminal(**opts.slice(:color, :colors))
316
356
  handle_errors do
317
- content = read_section_body(from: from, body: body, stdin: stdin)
357
+ content = read_body(from: from, body: body, stdin: stdin, what: "section body")
318
358
  render(store.find(space)) do |sp|
319
359
  project = ArchitectProject.new(space: sp)
320
- res = project.write_section!(iteration, section, body: content, append: append, lane: lane)
360
+ res = project.write_section!(iteration, section, body: content, append: append, lane: lane,
361
+ message: read_commit_message(message: message, message_from: message_from))
321
362
  if res[:committed]
322
363
  terminal.say "Committed #{res[:heading]} → #{res[:sha][0, 8]}"
323
364
  terminal.say res[:diffstat] unless res[:diffstat].empty?
@@ -328,15 +369,6 @@ module Space::Architect
328
369
  end
329
370
  end
330
371
  end
331
-
332
- private
333
-
334
- def read_section_body(from:, body:, stdin:)
335
- return File.read(from) if from
336
- return body if body
337
- return $stdin.read if stdin
338
- raise Space::Core::Error, "provide the section body via --from <file>, --body <text>, or --stdin"
339
- end
340
372
  end
341
373
 
342
374
  class Verdict < BaseCommand
@@ -348,28 +380,22 @@ module Space::Architect
348
380
  option :from, default: nil, desc: "Read the verdict body from this file"
349
381
  option :body, default: nil, desc: "Inline verdict body"
350
382
  option :stdin, type: :boolean, default: false, desc: "Read the verdict body from stdin"
383
+ commit_message_options
351
384
 
352
- def call(iteration:, decision:, space: nil, from: nil, body: nil, stdin: false, **opts)
385
+ def call(iteration:, decision:, space: nil, from: nil, body: nil, stdin: false,
386
+ message: nil, message_from: nil, **opts)
353
387
  setup_terminal(**opts.slice(:color, :colors))
354
388
  handle_errors do
355
- content = read_section_body(from: from, body: body, stdin: stdin)
389
+ content = read_body(from: from, body: body, stdin: stdin, what: "verdict body")
356
390
  render(store.find(space)) do |sp|
357
391
  project = ArchitectProject.new(space: sp)
358
- res = project.record_verdict!(iteration, decision: decision, body: content)
392
+ res = project.record_verdict!(iteration, decision: decision, body: content,
393
+ message: read_commit_message(message: message, message_from: message_from))
359
394
  terminal.say "Verdict '#{res[:decision]}' recorded → #{res[:sha][0, 8]}"
360
395
  CLI.record_outcome(Outcome.new(exit_code: 0))
361
396
  end
362
397
  end
363
398
  end
364
-
365
- private
366
-
367
- def read_section_body(from:, body:, stdin:)
368
- return File.read(from) if from
369
- return body if body
370
- return $stdin.read if stdin
371
- raise Space::Core::Error, "provide the verdict body via --from <file>, --body <text>, or --stdin"
372
- end
373
399
  end
374
400
 
375
401
  class Evidence < BaseCommand
@@ -378,13 +404,15 @@ module Space::Architect
378
404
  argument :iteration, required: true, desc: "Iteration name"
379
405
  argument :space, required: false, desc: "Space identifier (default: $PWD)"
380
406
  option :lane, default: nil, desc: "Lane name (per-lane subsection; omit for a single-lane iteration)"
407
+ commit_message_options
381
408
 
382
- def call(iteration:, space: nil, lane: nil, **opts)
409
+ def call(iteration:, space: nil, lane: nil, message: nil, message_from: nil, **opts)
383
410
  setup_terminal(**opts.slice(:color, :colors))
384
411
  handle_errors do
385
412
  render(store.find(space)) do |sp|
386
413
  project = ArchitectProject.new(space: sp)
387
- res = project.transcribe_evidence!(iteration, lane: lane)
414
+ res = project.transcribe_evidence!(iteration, lane: lane,
415
+ message: read_commit_message(message: message, message_from: message_from))
388
416
  terminal.say "Transcribed #{res[:lines]} lines → #{res[:sha][0, 8]}"
389
417
  terminal.say "Builder STATUS: #{res[:status_line]}" if res[:status_line]
390
418
  terminal.say "Now rule on the builder's PHASE 0 disagreements in the Verdict (a later session)."
@@ -400,14 +428,15 @@ module Space::Architect
400
428
  argument :iteration, required: true, desc: "Iteration name"
401
429
  argument :lane, required: true, desc: "Lane name (architect-judged passing)"
402
430
  argument :space, required: false, desc: "Space identifier (default: $PWD)"
403
- option :message, default: nil, desc: "Commit message for the lane's working-tree changes"
431
+ commit_message_options
404
432
 
405
- def call(iteration:, lane:, space: nil, message: nil, **opts)
433
+ def call(iteration:, lane:, space: nil, message: nil, message_from: nil, **opts)
406
434
  setup_terminal(**opts.slice(:color, :colors))
407
435
  handle_errors do
408
436
  render(store.find(space)) do |sp|
409
437
  project = ArchitectProject.new(space: sp)
410
- r = project.merge_lane!(iteration, lane, message: message)
438
+ r = project.merge_lane!(iteration, lane,
439
+ message: read_commit_message(message: message, message_from: message_from))
411
440
  terminal.say "Merged #{lane} → #{r[:integration_branch]} (#{r[:merge_sha][0, 8]})"
412
441
  terminal.say r[:diffstat] unless r[:diffstat].empty?
413
442
  terminal.say "Gates NOT run — run `architect gate #{iteration}` against the integration branch."
@@ -424,8 +453,9 @@ module Space::Architect
424
453
  argument :space, required: false, desc: "Space identifier (default: $PWD)"
425
454
  option :lanes, required: false, desc: "Comma-separated passing lane names (you decide the set)"
426
455
  option :teardown, type: :boolean, default: false, desc: "Remove worktrees + delete lane branches after merge"
456
+ commit_message_options
427
457
 
428
- def call(iteration:, space: nil, lanes: nil, teardown: false, **opts)
458
+ def call(iteration:, space: nil, lanes: nil, teardown: false, message: nil, message_from: nil, **opts)
429
459
  setup_terminal(**opts.slice(:color, :colors))
430
460
  handle_errors do
431
461
  lane_names = lanes.to_s.split(",").map(&:strip).reject(&:empty?)
@@ -434,7 +464,8 @@ module Space::Architect
434
464
 
435
465
  render(store.find(space)) do |sp|
436
466
  project = ArchitectProject.new(space: sp)
437
- results = project.integrate!(iteration, lanes: lane_names, teardown: teardown)
467
+ results = project.integrate!(iteration, lanes: lane_names, teardown: teardown,
468
+ message: read_commit_message(message: message, message_from: message_from))
438
469
  if lane_names.empty?
439
470
  if results.empty?
440
471
  terminal.say "Nothing to tear down for #{iteration}"
@@ -690,17 +721,23 @@ module Space::Architect
690
721
 
691
722
  module Brief
692
723
  class New < BaseCommand
693
- desc "Scaffold the durable project brief (architecture/BRIEF.md)"
724
+ desc "Write the durable project brief (architecture/BRIEF.md) — authored via --from/--stdin, or a placeholder template"
694
725
  argument :space, required: false, desc: "Space identifier (default: $PWD)"
695
726
  option :force, type: :boolean, default: false, desc: "Overwrite an existing BRIEF.md"
727
+ option :from, default: nil, desc: "Read the authored brief body from this file"
728
+ option :stdin, type: :boolean, default: false, desc: "Read the authored brief body from stdin"
729
+ commit_message_options
696
730
 
697
- def call(space: nil, force: false, **opts)
731
+ def call(space: nil, force: false, from: nil, stdin: false, message: nil, message_from: nil, **opts)
698
732
  setup_terminal(**opts.slice(:color, :colors))
699
733
  handle_errors do
734
+ content = (from || stdin) ? read_body(from: from, stdin: stdin, what: "brief body") : nil
700
735
  render(store.find(space)) do |sp|
701
736
  project = ArchitectProject.new(space: sp)
702
- path = project.brief_new!(force: force)
703
- terminal.say "Brief ready: #{terminal.path(path)}"
737
+ path = project.brief_new!(force: force, content: content,
738
+ message: read_commit_message(message: message, message_from: message_from))
739
+ note = content ? "" : " (template — Read it before editing)"
740
+ terminal.say "Brief ready: #{terminal.path(path)}#{note}"
704
741
  CLI.record_outcome(Outcome.new(exit_code: 0))
705
742
  end
706
743
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Space
4
4
  module Core
5
- VERSION = "4.0.0"
5
+ VERSION = "5.0.0"
6
6
  end
7
7
  end
@@ -46,7 +46,20 @@ message, and prints back what changed (SHA + diff stat; `freeze` prints the
46
46
  frozen AC; `evidence` echoes the builder's STATUS line) — so you don't hand-edit
47
47
  the file or run a separate `git add`/`commit`, and you don't run three follow-ups
48
48
  to see what happened. You still author the *content*; the CLI owns the
49
- *persistence*.
49
+ *persistence*. Two disciplines fall out of that split:
50
+
51
+ - **Author in fresh files; canonical paths are CLI-owned.** Write your content
52
+ to a fresh scratch file (timestamp it — `tmp/` in the space) and hand it to
53
+ the command via `--from`/`--prompt`/`--stdin`. Never `Write`/`Edit` a path you
54
+ haven't read this session — the pre-existing file trips your harness's
55
+ read-before-write guard and burns a failed-write → read → rewrite round trip.
56
+ - **Write detailed commit messages.** Every committing command takes
57
+ `-m`/`--message` and `--message-from <file>`: your first line completes the
58
+ subject after a short canonical prefix (`I01 spec: <your subject>`), the rest
59
+ becomes the body. The space's git log is the loop's durable memory — record
60
+ the *why* (rejected alternatives, BRIEF §refs, judgment reasoning) there, not
61
+ just in your head. Prefer `--message-from <fresh file>` for multiline bodies
62
+ (shells mangle quotes).
50
63
 
51
64
  The builder **never** writes this file — the Acceptance Criteria must stay out
52
65
  of its editable blast radius. Each lane builder writes raw evidence to a scratch
@@ -62,7 +75,9 @@ of done) that span iterations. Every iteration's Grounds/Specification/Acceptanc
62
75
  Criteria/Verdict cites it as **BRIEF §N** (e.g. `(BRIEF §3.1)`), the way each gate
63
76
  addresses its intent back to one frozen reference: the Acceptance Criteria table
64
77
  carries a `Brief §` column, the Specification Objective cites it, the Verdict
65
- reads "diff vs BRIEF §1/§3.3 — CONTINUE". Scaffold it with `architect brief new`.
78
+ reads "diff vs BRIEF §1/§3.3 — CONTINUE". Author it in a fresh scratch file and
79
+ persist with `architect brief new --from <file>` (bare `architect brief new`
80
+ drops a placeholder template for a human to fill).
66
81
  The brief is frozen at the project level — edits to a §section are logged
67
82
  decisions in `ARCHITECT.md`, never silent per-iteration drift. Discovery projects
68
83
  that are still finding their shape defer the brief, cite per-iteration Grounds,
@@ -325,10 +340,14 @@ dispatch-in-the-checkout path:
325
340
 
326
341
  Assemble each lane's lane-prompt (the template in `dispatch.md` + this lane's
327
342
  section of the Specification + the frozen Acceptance Criteria) and write it to
328
- `build/<id>-<lane>/prompt.md` (fed to the builder on stdin); record it in the
343
+ a **fresh timestamped scratch file**, `tmp/prompts/<id>-<lane>-<hhmmss>.md`
344
+ never to `build/<id>-<lane>/prompt.md` directly (a pre-existing canonical file
345
+ trips your harness's read-before-write guard). Record it in the
329
346
  iteration file's **Builder Prompt** section — the dispatched-prompt provenance —
330
347
  with `architect section <iteration> prompt --append --lane <lane> --from
331
- build/<id>-<lane>/prompt.md`. Then run `architect dispatch <iteration> <lane>` — it assembles the
348
+ <that scratch file>`. Then run `architect dispatch <iteration> <lane> --prompt
349
+ <that scratch file>` — it copies the prompt to the canonical
350
+ `build/<id>-<lane>/prompt.md` (fed to the builder on stdin), assembles the
332
351
  canonical `claude -p` argv, pins the model, and streams stream-json to
333
352
  `build/<id>-<lane>/run.jsonl`. Launch one dispatch per worktree — each as its
334
353
  **own background Bash tool call** (your harness's `run_in_background`), **not**
@@ -399,7 +418,9 @@ PASS/FAIL/INVALID, the KILL/CONTINUE call.
399
418
  At project end, landing is yours, not the CLI's — the PR body is judgment
400
419
  output, the same class as a Verdict. Per touched repo: write the PR body
401
420
  yourself — from the iteration verdicts, the integrated diff, and the BRIEF —
402
- to `build/land/<repo>-pr-body.md`, then present the paste-and-run block to the
421
+ to a fresh timestamped file `build/land/<repo>-pr-body-<yyyymmdd-hhmm>.md`
422
+ (never rewrite a prior session's body in place — fresh file, fresh Write),
423
+ then present the paste-and-run block to the
403
424
  human: `cd` to the repo checkout, `git push -u origin project/<slug>`, and
404
425
  `gh pr create --base main --head project/<slug> --title … --body-file …` —
405
426
  paths `~`-contracted, the multi-flag command broken with trailing ` \` at flag
@@ -39,22 +39,27 @@ with the pinned `<builder-model>` or the log isn't growing.
39
39
 
40
40
  ## Canonical dispatch — `architect dispatch <iteration> <lane>`
41
41
 
42
- The canonical path is `architect dispatch <iteration> <lane>`. The tool
43
- assembles the canonical `claude -p` argv, pins the builder model (the lane's
42
+ The canonical path is `architect dispatch <iteration> <lane> --prompt <file>`.
43
+ The tool copies your prompt file to `build/<id>-<lane>/prompt.md` (the CLI owns
44
+ that canonical path — you never write it directly), assembles the canonical
45
+ `claude -p` argv, pins the builder model (the lane's
44
46
  configured model or the CLI's reference default; see `docs/DESIGN.md` §4),
45
- reads the lane prompt from `build/<id>-<lane>/prompt.md` on stdin, and streams
47
+ feeds the copied lane prompt to the builder on stdin, and streams
46
48
  `--output-format stream-json --verbose` output to
47
49
  `build/<id>-<lane>/run.jsonl`. Run each lane as its own **background Bash tool
48
50
  call** (`run_in_background`) so your turn doesn't block for the full run (30–60
49
51
  minutes is typical).
50
52
 
51
- Write the lane's prompt to `build/<id>-<lane>/prompt.md` first (never pass a
52
- big prompt as a shell argument — shells mangle quotes), then:
53
+ Author the lane's prompt in a **fresh timestamped scratch file**
54
+ (`tmp/prompts/<id>-<lane>-<hhmmss>.md` never a pre-existing canonical path,
55
+ which trips your harness's read-before-write guard; never a shell argument —
56
+ shells mangle quotes), then hand it to dispatch:
53
57
 
54
58
  ```bash
55
- # single-lane iteration — run from the space root; dispatch runs in the lane's
56
- # provisioned worktree (materialized on demand from the frozen declaration)
57
- architect dispatch <iteration> <lane>
59
+ # single-lane iteration — run from the space root; dispatch copies the prompt to
60
+ # build/<id>-<lane>/prompt.md and runs in the lane's provisioned worktree
61
+ # (materialized on demand from the frozen declaration)
62
+ architect dispatch <iteration> <lane> --prompt tmp/prompts/<id>-<lane>-<hhmmss>.md
58
63
  ```
59
64
 
60
65
  For multi-lane iterations, materialize every declared lane in one shot with
@@ -62,8 +67,8 @@ For multi-lane iterations, materialize every declared lane in one shot with
62
67
 
63
68
  ```bash
64
69
  architect provision <iteration> # all declared lanes: worktree + lane/<id>-<lane> branch
65
- architect dispatch <iteration> lane-a # own background Bash call each
66
- architect dispatch <iteration> lane-b
70
+ architect dispatch <iteration> lane-a --prompt <lane-a scratch file> # own background Bash call each
71
+ architect dispatch <iteration> lane-b --prompt <lane-b scratch file>
67
72
  ```
68
73
 
69
74
  `architect provision` reads the frozen lane declarations from `space.yaml` and,
@@ -92,7 +97,8 @@ survives the full run and reports completion per lane.
92
97
  and as the manual fallback:
93
98
 
94
99
  ```bash
95
- # write prompt to build/<id>-<lane>/prompt.md first, then:
100
+ # dispatch --prompt copies the scratch prompt to build/<id>-<lane>/prompt.md; the
101
+ # manual equivalent is that copy followed by:
96
102
  ( cd build/<id>-<lane>/wt && \
97
103
  claude -p --model <builder-model> \
98
104
  --permission-mode acceptEdits \
@@ -123,13 +129,16 @@ architect integrate <iteration> --lanes <passing-set> # e.g. --lanes lane-a,la
123
129
  architect gate <iteration> # integration smoke (raw output; verdict stays yours)
124
130
  architect integrate <iteration> --lanes <passing-set> --teardown # or remove worktrees + lane branches after
125
131
  # end of project: landing is the architect's, not a CLI command — write the PR
126
- # body to build/land/<repo>-pr-body.md yourself, then present the paste-and-run
127
- # block (cd, git push -u origin project/<slug>, gh pr create) — see SKILL.md §6
132
+ # body to a fresh build/land/<repo>-pr-body-<yyyymmdd-hhmm>.md yourself, then
133
+ # present the paste-and-run block (cd, git push -u origin project/<slug>,
134
+ # gh pr create) — see SKILL.md §6
128
135
  ```
129
136
 
130
137
  `architect integrate` commits each named lane on its branch and merges it
131
138
  `--no-ff` into the repo's stable `project/<slug>` branch (slug of `space.title`,
132
- persistent across all iterations), in order. It **refuses** a lane that left
139
+ persistent across all iterations), in order. Pass `-m`/`--message-from` the
140
+ lane commit lands in the repo's PR history, so say what the lane did and why,
141
+ not just that it integrated. It **refuses** a lane that left
133
142
  builder commits or wrote out-of-bounds (the mechanical post-flight checks), and
134
143
  aborts on a merge conflict. A merge conflict = the lane plan wasn't disjoint = a
135
144
  spec defect: kill the conflicting lane and re-spec; don't hand-resolve builder
@@ -150,8 +159,9 @@ git -C repos/<repo> merge --no-ff lane/<iteration>-<lane>
150
159
  <run the gate commands> # integration smoke after every merge
151
160
  architect worktree remove <iteration> <lane>
152
161
  git -C repos/<repo> branch -d lane/<iteration>-<lane>
153
- # at project end there is no CLI step: the architect writes
154
- # build/land/<repo>-pr-body.md and presents the push + gh pr create block
162
+ # at project end there is no CLI step: the architect writes a fresh
163
+ # build/land/<repo>-pr-body-<yyyymmdd-hhmm>.md and presents the push +
164
+ # gh pr create block
155
165
  ```
156
166
 
157
167
  ### Parallel + fast-follow
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: 4.0.0
4
+ version: 5.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Eric Jacobs