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 +4 -4
- data/CHANGELOG.md +86 -0
- data/README.md +11 -7
- data/lib/space_architect/architect_project.rb +65 -32
- data/lib/space_architect/cli/architect.rb +79 -42
- data/lib/space_core/version.rb +1 -1
- data/skill/architect/SKILL.md +26 -5
- data/skill/architect/dispatch.md +26 -16
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6846eab011f92f0b54a7bb886d7b97cf8c727beb3fc6d279a2db603062645520
|
|
4
|
+
data.tar.gz: 0bc8f0b22f785fd2d3ed77419a1ad3b6058e4b76a6aaee41b7a66793b865702a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
275
|
-
|
|
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
|
|
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>
|
|
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
|
|
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
|
-
#
|
|
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
|
-
|
|
101
|
-
git_run("-C", space.path.to_s, "commit", "-m",
|
|
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",
|
|
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",
|
|
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
|
-
|
|
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
|
-
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
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,
|
|
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 =
|
|
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,
|
|
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 =
|
|
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
|
-
|
|
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,
|
|
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 "
|
|
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
|
-
|
|
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
|
data/lib/space_core/version.rb
CHANGED
data/skill/architect/SKILL.md
CHANGED
|
@@ -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".
|
|
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
|
-
`
|
|
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
|
-
|
|
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
|
|
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
|
data/skill/architect/dispatch.md
CHANGED
|
@@ -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
|
|
43
|
-
|
|
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
|
-
|
|
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
|
-
|
|
52
|
-
|
|
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
|
|
56
|
-
#
|
|
57
|
-
|
|
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
|
|
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
|
-
#
|
|
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
|
|
127
|
-
# block (cd, git push -u origin project/<slug>,
|
|
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.
|
|
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
|
|
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
|