space-architect 4.0.0 → 5.1.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 +137 -0
- data/README.md +11 -7
- data/lib/space_architect/architect_project.rb +236 -70
- data/lib/space_architect/cli/architect.rb +128 -60
- data/lib/space_core/cli/run.rb +3 -2
- data/lib/space_core/version.rb +1 -1
- data/skill/architect/SKILL.md +26 -5
- data/skill/architect/dispatch.md +26 -16
- metadata +15 -1
|
@@ -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
|
|
@@ -141,19 +174,41 @@ module Space::Architect
|
|
|
141
174
|
end
|
|
142
175
|
end
|
|
143
176
|
|
|
177
|
+
class Sync < BaseCommand
|
|
178
|
+
desc "Sync tracked repo clones with their remotes (fast-forward only, no rebase/reset)"
|
|
179
|
+
phase 53, "Project"
|
|
180
|
+
argument :repo, required: false, desc: "Repo name to sync (default: all tracked repos)"
|
|
181
|
+
argument :space, required: false, desc: "Space identifier (default: $PWD)"
|
|
182
|
+
|
|
183
|
+
def call(repo: nil, space: nil, **opts)
|
|
184
|
+
setup_terminal(**opts.slice(:color, :colors))
|
|
185
|
+
handle_errors do
|
|
186
|
+
render(store.find(space)) do |sp|
|
|
187
|
+
project = ArchitectProject.new(space: sp)
|
|
188
|
+
results = project.sync_repos(repo_name: repo)
|
|
189
|
+
results.each { |r| terminal.say r[:message] }
|
|
190
|
+
CLI.record_outcome(Outcome.new(exit_code: 0))
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
144
196
|
class Freeze < BaseCommand
|
|
145
197
|
desc "Freeze the iteration's frozen region (Grounds/Specification/Acceptance Criteria) and record the freeze SHA"
|
|
146
198
|
phase 12, "Spec"
|
|
147
199
|
argument :iteration, required: true, desc: "Iteration name"
|
|
148
200
|
argument :space, required: false, desc: "Space identifier (default: $PWD)"
|
|
201
|
+
option :force, type: :boolean, default: false, desc: "Re-freeze even if the frozen region changed (pre-dispatch only)"
|
|
202
|
+
commit_message_options
|
|
149
203
|
|
|
150
|
-
def call(iteration:, space: nil, **opts)
|
|
204
|
+
def call(iteration:, space: nil, message: nil, message_from: nil, force: false, **opts)
|
|
151
205
|
setup_terminal(**opts.slice(:color, :colors))
|
|
152
206
|
handle_errors do
|
|
153
207
|
render(store.find(space)) do |sp|
|
|
154
208
|
project = ArchitectProject.new(space: sp)
|
|
155
209
|
warnings = []
|
|
156
|
-
sha = project.freeze!(iteration, warnings: warnings
|
|
210
|
+
sha = project.freeze!(iteration, warnings: warnings, force: force,
|
|
211
|
+
message: read_commit_message(message: message, message_from: message_from))
|
|
157
212
|
terminal.say "Frozen #{iteration} at #{sha}"
|
|
158
213
|
warnings.each { |w| terminal.say "Warning: #{w}" }
|
|
159
214
|
ac = project.acceptance_criteria(iteration)
|
|
@@ -171,15 +226,17 @@ module Space::Architect
|
|
|
171
226
|
class Verify < BaseCommand
|
|
172
227
|
desc "Post-flight mechanical lane checks — frozen-untouched, no builder commits, report exists, in-bounds (reports only, no judgment)"
|
|
173
228
|
phase 30, "Judge"
|
|
174
|
-
argument :iteration,
|
|
175
|
-
argument :space,
|
|
229
|
+
argument :iteration, required: true, desc: "Iteration name"
|
|
230
|
+
argument :space, required: false, desc: "Space identifier (default: $PWD)"
|
|
231
|
+
option :commit_mode, default: nil, desc: "Commit mode override (strict|conductor); overrides space.yaml commit_mode for this run"
|
|
176
232
|
|
|
177
|
-
def call(iteration:, space: nil, **opts)
|
|
233
|
+
def call(iteration:, space: nil, commit_mode: nil, **opts)
|
|
178
234
|
setup_terminal(**opts.slice(:color, :colors))
|
|
179
235
|
handle_errors do
|
|
180
236
|
render(store.find(space)) do |sp|
|
|
181
237
|
project = ArchitectProject.new(space: sp)
|
|
182
|
-
|
|
238
|
+
terminal.say "Effective commit_mode: #{commit_mode}" if commit_mode
|
|
239
|
+
results = project.verify(iteration, commit_mode: commit_mode)
|
|
183
240
|
|
|
184
241
|
if results.empty?
|
|
185
242
|
terminal.say "No lanes recorded for iteration '#{iteration}'"
|
|
@@ -222,6 +279,7 @@ module Space::Architect
|
|
|
222
279
|
argument :iteration, required: true, desc: "Iteration name"
|
|
223
280
|
argument :lane, required: true, desc: "Lane name"
|
|
224
281
|
argument :space, required: false, desc: "Space identifier (default: $PWD)"
|
|
282
|
+
option :prompt, default: nil, desc: "Read the lane prompt from this file (copied byte-for-byte to build/<id>-<lane>/prompt.md)"
|
|
225
283
|
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
284
|
option :max_turns, default: "200", desc: "Max turns for the builder"
|
|
227
285
|
option :harness, default: nil, desc: "Harness override (claude-code, opencode)"
|
|
@@ -232,7 +290,7 @@ module Space::Architect
|
|
|
232
290
|
option :push_token, default: nil, desc: "Bearer token for push endpoint authorization"
|
|
233
291
|
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
292
|
|
|
235
|
-
def call(iteration:, lane:, space: nil, model: nil,
|
|
293
|
+
def call(iteration:, lane:, space: nil, prompt: nil, model: nil,
|
|
236
294
|
max_turns: "200", harness: nil, effort: nil, detach: false,
|
|
237
295
|
timeout: "14400", push_url: nil, push_token: nil, push_host: nil, **opts)
|
|
238
296
|
setup_terminal(**opts.slice(:color, :colors))
|
|
@@ -240,6 +298,7 @@ module Space::Architect
|
|
|
240
298
|
render(store.find(space)) do |sp|
|
|
241
299
|
project = ArchitectProject.new(space: sp)
|
|
242
300
|
kwargs = { max_turns: max_turns.to_i, detach: detach }
|
|
301
|
+
kwargs[:prompt] = prompt if prompt
|
|
243
302
|
kwargs[:model] = model if model
|
|
244
303
|
kwargs[:harness] = harness if harness
|
|
245
304
|
kwargs[:effort] = effort if effort
|
|
@@ -248,6 +307,7 @@ module Space::Architect
|
|
|
248
307
|
kwargs[:push_token] = push_token if push_token
|
|
249
308
|
kwargs[:push_host] = push_host if push_host
|
|
250
309
|
res = project.dispatch(iteration, lane, **kwargs)
|
|
310
|
+
terminal.say "Prompt: #{prompt} → #{terminal.path(res[:prompt_copied])}" if res[:prompt_copied]
|
|
251
311
|
if detach
|
|
252
312
|
terminal.say "PID: #{res[:pid]}"
|
|
253
313
|
terminal.say "Run log: #{terminal.path(res[:run_log])}"
|
|
@@ -278,13 +338,14 @@ module Space::Architect
|
|
|
278
338
|
argument :space, required: false, desc: "Space identifier (default: $PWD)"
|
|
279
339
|
option :base, default: nil, desc: "Base ref override (default: project/<slug> HEAD if it exists, else the repo's default branch)"
|
|
280
340
|
option :lane, default: nil, desc: "Provision only this lane (default: all declared lanes)"
|
|
341
|
+
option :force, type: :boolean, default: false, desc: "Clear and re-create a stale (unregistered) worktree directory"
|
|
281
342
|
|
|
282
|
-
def call(iteration:, space: nil, base: nil, lane: nil, **opts)
|
|
343
|
+
def call(iteration:, space: nil, base: nil, lane: nil, force: false, **opts)
|
|
283
344
|
setup_terminal(**opts.slice(:color, :colors))
|
|
284
345
|
handle_errors do
|
|
285
346
|
render(store.find(space)) do |sp|
|
|
286
347
|
project = ArchitectProject.new(space: sp)
|
|
287
|
-
results = project.provision(iteration, base: base, lane: lane)
|
|
348
|
+
results = project.provision(iteration, base: base, lane: lane, force: force)
|
|
288
349
|
if results.empty?
|
|
289
350
|
terminal.say "No declared lanes to provision for '#{iteration}'"
|
|
290
351
|
else
|
|
@@ -303,21 +364,25 @@ module Space::Architect
|
|
|
303
364
|
desc "Write a section of the iteration file and commit it (one call)"
|
|
304
365
|
phase 11, "Spec"
|
|
305
366
|
argument :iteration, required: true, desc: "Iteration name"
|
|
306
|
-
argument :section, required: true, desc: "Section: grounds, specification, prompt, verdict"
|
|
367
|
+
argument :section, required: true, desc: "Section: grounds, specification, acceptance-criteria, prompt, verdict"
|
|
307
368
|
argument :space, required: false, desc: "Space identifier (default: $PWD)"
|
|
308
369
|
option :from, default: nil, desc: "Read the section body from this file"
|
|
309
370
|
option :body, default: nil, desc: "Inline section body (one-liners)"
|
|
310
371
|
option :stdin, type: :boolean, default: false, desc: "Read the section body from stdin"
|
|
311
372
|
option :append, type: :boolean, default: false, desc: "Append a ### <lane> subsection instead of replacing"
|
|
312
373
|
option :lane, default: nil, desc: "Lane name for an appended ### subsection"
|
|
374
|
+
option :force, type: :boolean, default: false, desc: "Write a frozen section (pre-dispatch only)"
|
|
375
|
+
commit_message_options
|
|
313
376
|
|
|
314
|
-
def call(iteration:, section:, space: nil, from: nil, body: nil, stdin: false, append: false, lane: nil,
|
|
377
|
+
def call(iteration:, section:, space: nil, from: nil, body: nil, stdin: false, append: false, lane: nil,
|
|
378
|
+
message: nil, message_from: nil, force: false, **opts)
|
|
315
379
|
setup_terminal(**opts.slice(:color, :colors))
|
|
316
380
|
handle_errors do
|
|
317
|
-
content =
|
|
381
|
+
content = read_body(from: from, body: body, stdin: stdin, what: "section body")
|
|
318
382
|
render(store.find(space)) do |sp|
|
|
319
383
|
project = ArchitectProject.new(space: sp)
|
|
320
|
-
res = project.write_section!(iteration, section, body: content, append: append, lane: lane
|
|
384
|
+
res = project.write_section!(iteration, section, body: content, append: append, lane: lane, force: force,
|
|
385
|
+
message: read_commit_message(message: message, message_from: message_from))
|
|
321
386
|
if res[:committed]
|
|
322
387
|
terminal.say "Committed #{res[:heading]} → #{res[:sha][0, 8]}"
|
|
323
388
|
terminal.say res[:diffstat] unless res[:diffstat].empty?
|
|
@@ -328,15 +393,6 @@ module Space::Architect
|
|
|
328
393
|
end
|
|
329
394
|
end
|
|
330
395
|
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
396
|
end
|
|
341
397
|
|
|
342
398
|
class Verdict < BaseCommand
|
|
@@ -348,28 +404,22 @@ module Space::Architect
|
|
|
348
404
|
option :from, default: nil, desc: "Read the verdict body from this file"
|
|
349
405
|
option :body, default: nil, desc: "Inline verdict body"
|
|
350
406
|
option :stdin, type: :boolean, default: false, desc: "Read the verdict body from stdin"
|
|
407
|
+
commit_message_options
|
|
351
408
|
|
|
352
|
-
def call(iteration:, decision:, space: nil, from: nil, body: nil, stdin: false,
|
|
409
|
+
def call(iteration:, decision:, space: nil, from: nil, body: nil, stdin: false,
|
|
410
|
+
message: nil, message_from: nil, **opts)
|
|
353
411
|
setup_terminal(**opts.slice(:color, :colors))
|
|
354
412
|
handle_errors do
|
|
355
|
-
content =
|
|
413
|
+
content = read_body(from: from, body: body, stdin: stdin, what: "verdict body")
|
|
356
414
|
render(store.find(space)) do |sp|
|
|
357
415
|
project = ArchitectProject.new(space: sp)
|
|
358
|
-
res = project.record_verdict!(iteration, decision: decision, body: content
|
|
416
|
+
res = project.record_verdict!(iteration, decision: decision, body: content,
|
|
417
|
+
message: read_commit_message(message: message, message_from: message_from))
|
|
359
418
|
terminal.say "Verdict '#{res[:decision]}' recorded → #{res[:sha][0, 8]}"
|
|
360
419
|
CLI.record_outcome(Outcome.new(exit_code: 0))
|
|
361
420
|
end
|
|
362
421
|
end
|
|
363
422
|
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
423
|
end
|
|
374
424
|
|
|
375
425
|
class Evidence < BaseCommand
|
|
@@ -378,13 +428,15 @@ module Space::Architect
|
|
|
378
428
|
argument :iteration, required: true, desc: "Iteration name"
|
|
379
429
|
argument :space, required: false, desc: "Space identifier (default: $PWD)"
|
|
380
430
|
option :lane, default: nil, desc: "Lane name (per-lane subsection; omit for a single-lane iteration)"
|
|
431
|
+
commit_message_options
|
|
381
432
|
|
|
382
|
-
def call(iteration:, space: nil, lane: nil, **opts)
|
|
433
|
+
def call(iteration:, space: nil, lane: nil, message: nil, message_from: nil, **opts)
|
|
383
434
|
setup_terminal(**opts.slice(:color, :colors))
|
|
384
435
|
handle_errors do
|
|
385
436
|
render(store.find(space)) do |sp|
|
|
386
437
|
project = ArchitectProject.new(space: sp)
|
|
387
|
-
res = project.transcribe_evidence!(iteration, lane: lane
|
|
438
|
+
res = project.transcribe_evidence!(iteration, lane: lane,
|
|
439
|
+
message: read_commit_message(message: message, message_from: message_from))
|
|
388
440
|
terminal.say "Transcribed #{res[:lines]} lines → #{res[:sha][0, 8]}"
|
|
389
441
|
terminal.say "Builder STATUS: #{res[:status_line]}" if res[:status_line]
|
|
390
442
|
terminal.say "Now rule on the builder's PHASE 0 disagreements in the Verdict (a later session)."
|
|
@@ -397,17 +449,20 @@ module Space::Architect
|
|
|
397
449
|
class Merge < BaseCommand
|
|
398
450
|
desc "Integrate ONE judged-passing lane (merges --no-ff; runs no gates, makes no verdict)"
|
|
399
451
|
phase 41, "Land"
|
|
400
|
-
argument :iteration,
|
|
401
|
-
argument :lane,
|
|
402
|
-
argument :space,
|
|
403
|
-
option :
|
|
404
|
-
|
|
405
|
-
|
|
452
|
+
argument :iteration, required: true, desc: "Iteration name"
|
|
453
|
+
argument :lane, required: true, desc: "Lane name (architect-judged passing)"
|
|
454
|
+
argument :space, required: false, desc: "Space identifier (default: $PWD)"
|
|
455
|
+
option :into, required: false, desc: "Merge into this branch instead of the slug-derived project/<slug> default"
|
|
456
|
+
option :commit_mode, default: nil, desc: "Commit mode override (strict|conductor); overrides space.yaml commit_mode for this run"
|
|
457
|
+
commit_message_options
|
|
458
|
+
|
|
459
|
+
def call(iteration:, lane:, space: nil, message: nil, message_from: nil, into: nil, commit_mode: nil, **opts)
|
|
406
460
|
setup_terminal(**opts.slice(:color, :colors))
|
|
407
461
|
handle_errors do
|
|
408
462
|
render(store.find(space)) do |sp|
|
|
409
463
|
project = ArchitectProject.new(space: sp)
|
|
410
|
-
r = project.merge_lane!(iteration, lane,
|
|
464
|
+
r = project.merge_lane!(iteration, lane, into: into, commit_mode: commit_mode,
|
|
465
|
+
message: read_commit_message(message: message, message_from: message_from))
|
|
411
466
|
terminal.say "Merged #{lane} → #{r[:integration_branch]} (#{r[:merge_sha][0, 8]})"
|
|
412
467
|
terminal.say r[:diffstat] unless r[:diffstat].empty?
|
|
413
468
|
terminal.say "Gates NOT run — run `architect gate #{iteration}` against the integration branch."
|
|
@@ -420,12 +475,15 @@ module Space::Architect
|
|
|
420
475
|
class Integrate < BaseCommand
|
|
421
476
|
desc "Integrate the architect-supplied set of passing lanes, in order (stops on conflict)"
|
|
422
477
|
phase 40, "Land"
|
|
423
|
-
argument :iteration,
|
|
424
|
-
argument :space,
|
|
425
|
-
option :lanes,
|
|
426
|
-
option :teardown,
|
|
427
|
-
|
|
428
|
-
|
|
478
|
+
argument :iteration, required: true, desc: "Iteration name"
|
|
479
|
+
argument :space, required: false, desc: "Space identifier (default: $PWD)"
|
|
480
|
+
option :lanes, required: false, desc: "Comma-separated passing lane names (you decide the set)"
|
|
481
|
+
option :teardown, type: :boolean, default: false, desc: "Remove worktrees + delete lane branches after merge"
|
|
482
|
+
option :commit_mode, default: nil, desc: "Commit mode override (strict|conductor); overrides space.yaml commit_mode for this run"
|
|
483
|
+
option :into, required: false, desc: "Merge into this branch instead of the slug-derived project/<slug> default"
|
|
484
|
+
commit_message_options
|
|
485
|
+
|
|
486
|
+
def call(iteration:, space: nil, lanes: nil, teardown: false, message: nil, message_from: nil, commit_mode: nil, into: nil, **opts)
|
|
429
487
|
setup_terminal(**opts.slice(:color, :colors))
|
|
430
488
|
handle_errors do
|
|
431
489
|
lane_names = lanes.to_s.split(",").map(&:strip).reject(&:empty?)
|
|
@@ -434,7 +492,9 @@ module Space::Architect
|
|
|
434
492
|
|
|
435
493
|
render(store.find(space)) do |sp|
|
|
436
494
|
project = ArchitectProject.new(space: sp)
|
|
437
|
-
results = project.integrate!(iteration, lanes: lane_names, teardown: teardown
|
|
495
|
+
results = project.integrate!(iteration, lanes: lane_names, teardown: teardown,
|
|
496
|
+
message: read_commit_message(message: message, message_from: message_from),
|
|
497
|
+
commit_mode: commit_mode, into: into)
|
|
438
498
|
if lane_names.empty?
|
|
439
499
|
if results.empty?
|
|
440
500
|
terminal.say "Nothing to tear down for #{iteration}"
|
|
@@ -543,15 +603,16 @@ module Space::Architect
|
|
|
543
603
|
option :model, default: nil, desc: "Model (required for opencode)"
|
|
544
604
|
option :effort, default: nil, desc: "Reasoning effort (opencode only; sets reasoningEffort in the model config)"
|
|
545
605
|
option :touch, default: nil, desc: "Comma-separated file globs the lane may touch (records its touch_set for in-bounds + merge checks)"
|
|
606
|
+
option :force, type: :boolean, default: false, desc: "Clear and re-create a stale (unregistered) worktree directory"
|
|
546
607
|
|
|
547
|
-
def call(repo:, iteration:, lane:, base: nil, harness: "claude-code", model: nil, effort: nil, touch: nil, **opts)
|
|
608
|
+
def call(repo:, iteration:, lane:, base: nil, harness: "claude-code", model: nil, effort: nil, touch: nil, force: false, **opts)
|
|
548
609
|
setup_terminal(**opts.slice(:color, :colors))
|
|
549
610
|
handle_errors do
|
|
550
611
|
render(store.find) do |sp|
|
|
551
612
|
project = ArchitectProject.new(space: sp)
|
|
552
613
|
touch_set = touch ? touch.split(",").map(&:strip).reject(&:empty?) : nil
|
|
553
614
|
result = project.worktree_add(repo, iteration, lane, base: base,
|
|
554
|
-
harness: harness, model: model, effort: effort, touch: touch_set)
|
|
615
|
+
harness: harness, model: model, effort: effort, touch: touch_set, force: force)
|
|
555
616
|
terminal.say "Worktree: #{terminal.path(result[:worktree])}"
|
|
556
617
|
terminal.say "Base SHA: #{result[:base_sha]}"
|
|
557
618
|
CLI.record_outcome(Outcome.new(exit_code: 0))
|
|
@@ -690,17 +751,23 @@ module Space::Architect
|
|
|
690
751
|
|
|
691
752
|
module Brief
|
|
692
753
|
class New < BaseCommand
|
|
693
|
-
desc "
|
|
754
|
+
desc "Write the durable project brief (architecture/BRIEF.md) — authored via --from/--stdin, or a placeholder template"
|
|
694
755
|
argument :space, required: false, desc: "Space identifier (default: $PWD)"
|
|
695
756
|
option :force, type: :boolean, default: false, desc: "Overwrite an existing BRIEF.md"
|
|
757
|
+
option :from, default: nil, desc: "Read the authored brief body from this file"
|
|
758
|
+
option :stdin, type: :boolean, default: false, desc: "Read the authored brief body from stdin"
|
|
759
|
+
commit_message_options
|
|
696
760
|
|
|
697
|
-
def call(space: nil, force: false, **opts)
|
|
761
|
+
def call(space: nil, force: false, from: nil, stdin: false, message: nil, message_from: nil, **opts)
|
|
698
762
|
setup_terminal(**opts.slice(:color, :colors))
|
|
699
763
|
handle_errors do
|
|
764
|
+
content = (from || stdin) ? read_body(from: from, stdin: stdin, what: "brief body") : nil
|
|
700
765
|
render(store.find(space)) do |sp|
|
|
701
766
|
project = ArchitectProject.new(space: sp)
|
|
702
|
-
path = project.brief_new!(force: force
|
|
703
|
-
|
|
767
|
+
path = project.brief_new!(force: force, content: content,
|
|
768
|
+
message: read_commit_message(message: message, message_from: message_from))
|
|
769
|
+
note = content ? "" : " (template — Read it before editing)"
|
|
770
|
+
terminal.say "Brief ready: #{terminal.path(path)}#{note}"
|
|
704
771
|
CLI.record_outcome(Outcome.new(exit_code: 0))
|
|
705
772
|
end
|
|
706
773
|
end
|
|
@@ -719,6 +786,7 @@ Space::Architect::CLI::Registry.register "init", Space::Architect::CLI::Archit
|
|
|
719
786
|
Space::Architect::CLI::Registry.register "ground", Space::Architect::CLI::Architect::Ground
|
|
720
787
|
Space::Architect::CLI::Registry.register "new", Space::Architect::CLI::Architect::New
|
|
721
788
|
Space::Architect::CLI::Registry.register "status", Space::Architect::CLI::Architect::Status
|
|
789
|
+
Space::Architect::CLI::Registry.register "sync", Space::Architect::CLI::Architect::Sync
|
|
722
790
|
Space::Architect::CLI::Registry.register "freeze", Space::Architect::CLI::Architect::Freeze
|
|
723
791
|
Space::Architect::CLI::Registry.register "verify", Space::Architect::CLI::Architect::Verify
|
|
724
792
|
Space::Architect::CLI::Registry.register "provision", Space::Architect::CLI::Architect::Provision
|
data/lib/space_core/cli/run.rb
CHANGED
|
@@ -5,11 +5,12 @@ require_relative "../oci_runner"
|
|
|
5
5
|
|
|
6
6
|
module Space::Core::CLI
|
|
7
7
|
class Run < BaseCommand
|
|
8
|
-
desc "Run the packed OCI image for the current space (auth injected at runtime)"
|
|
8
|
+
desc "Run the packed OCI image for the current space (auth injected at runtime). Pass a command and its arguments after `--` so they forward as separate argv tokens"
|
|
9
9
|
|
|
10
|
-
argument :command, type: :array, required: false, desc: "Command to run in the container (default: login shell)"
|
|
10
|
+
argument :command, type: :array, required: false, desc: "Command to run in the container (default: login shell). Use `--` to pass a command with arguments, e.g. `space run -- hermes -z \"hello\"`"
|
|
11
11
|
option :tty, type: :boolean, default: nil, desc: "Force interactive TTY (default: auto-detect)"
|
|
12
12
|
option :env, type: :array, desc: "Host env var to forward into the container (repeatable; adds to run.env)"
|
|
13
|
+
example "-- hermes -z \"What is 17 plus 4?\" # `--` forwards the command and its args as separate tokens (a quoted multi-word command arrives as one token and fails in-guest)"
|
|
13
14
|
|
|
14
15
|
def call(command: [], tty: nil, env: [], **opts)
|
|
15
16
|
setup_terminal(**opts.slice(:color, :colors))
|
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
|
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
|
+
version: 5.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Eric Jacobs
|
|
@@ -205,6 +205,20 @@ dependencies:
|
|
|
205
205
|
- - "~>"
|
|
206
206
|
- !ruby/object:Gem::Version
|
|
207
207
|
version: '6.0'
|
|
208
|
+
- !ruby/object:Gem::Dependency
|
|
209
|
+
name: mutant-minitest
|
|
210
|
+
requirement: !ruby/object:Gem::Requirement
|
|
211
|
+
requirements:
|
|
212
|
+
- - "~>"
|
|
213
|
+
- !ruby/object:Gem::Version
|
|
214
|
+
version: '0.16'
|
|
215
|
+
type: :development
|
|
216
|
+
prerelease: false
|
|
217
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
218
|
+
requirements:
|
|
219
|
+
- - "~>"
|
|
220
|
+
- !ruby/object:Gem::Version
|
|
221
|
+
version: '0.16'
|
|
208
222
|
- !ruby/object:Gem::Dependency
|
|
209
223
|
name: rake
|
|
210
224
|
requirement: !ruby/object:Gem::Requirement
|