space-architect 5.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 +51 -0
- data/lib/space_architect/architect_project.rb +175 -42
- data/lib/space_architect/cli/architect.rb +55 -24
- data/lib/space_core/cli/run.rb +3 -2
- data/lib/space_core/version.rb +1 -1
- metadata +15 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: '09d047544f9e1b3a29c56cc1e982a9624f72d45c2198429fcd7ab4564c31da41'
|
|
4
|
+
data.tar.gz: ea4e880ceda1c31ba11b390448d2380ca6d45d39394aeef2963c80a0e59a0901
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 01d09d4cd267afe17f189606d3620bdfa284802c4ccd506a2526172864b85c1124b8096aa3a5ed3fe61cfc3f18929d0aeb8f00d7d9c6238dfdf331ce1e2f8048
|
|
7
|
+
data.tar.gz: 313a4cf3c9dc69d5cb82d5d8bb1b95fa5d133de711d9ab5e320145525abddff4aebe8fc89cf58e0552d83147fae79ce697b567dce5a8ac387313f5064f58bf2b
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,57 @@ 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.1.0] - 2026-07-19
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **`architect integrate --into <branch>`** — merge a lane into a named branch
|
|
13
|
+
instead of the slug-derived `project/<slug>` default (#47). Outside-touch-set
|
|
14
|
+
conflict message gains an `--into` hint; inside-touch conflict keeps "spec
|
|
15
|
+
defect".
|
|
16
|
+
- **Conductor commit-mode** — `commit_mode: conductor` in `space.yaml` plus a
|
|
17
|
+
`--commit-mode` CLI flag on `verify`/`integrate`: canonical conductor commits
|
|
18
|
+
are classified as non-builder in the lane mechanical check, with canonical
|
|
19
|
+
message-shape matching (#55).
|
|
20
|
+
- **`architect sync` subcommand** (`--ff-only`, per-repo status) plus a
|
|
21
|
+
`ground` stale-repo WARNING with behind count and `--into` hint. **No
|
|
22
|
+
auto-sync** — the operator runs `sync` (#49).
|
|
23
|
+
- **`architect freeze --force` / `section --force`** — re-freeze an iteration
|
|
24
|
+
whose frozen region changed since the last freeze, or write a frozen section
|
|
25
|
+
after the freeze (pre-dispatch amend paths). Both refuse if any lane has
|
|
26
|
+
`dispatched_at` OR `integrate_sha` — moving `freeze_sha` or rewriting a frozen
|
|
27
|
+
section after a builder has run against the AC breaks the cardinal invariant
|
|
28
|
+
(AC freeze before results exist; judging quotes the freeze commit). The guard
|
|
29
|
+
names the offending lane and the reason.
|
|
30
|
+
- **`architect merge --into <branch>` / `--commit-mode <mode>`** — wires the
|
|
31
|
+
existing `merge_lane!` `into:`/`commit_mode:` kwargs through the `Merge` CLI,
|
|
32
|
+
matching `Integrate`'s surface.
|
|
33
|
+
- **`architect provision --force` / `worktree add --force`** — `worktree_add`
|
|
34
|
+
gains a `force: false` kwarg. When a worktree dir exists but is unknown to git
|
|
35
|
+
(stale, e.g. left by an aborted provision), `force: true` clears it via
|
|
36
|
+
`FileUtils.rm_rf` and re-creates; without `force` it still raises with a
|
|
37
|
+
`--force` hint so a genuine git-tracked dir is never silently destroyed.
|
|
38
|
+
`ensure_lane_materialized` (the auto-recovery path) stays non-force —
|
|
39
|
+
auto-recovery never silently `rm -rf`s; only an explicit operator `--force`.
|
|
40
|
+
|
|
41
|
+
### Changed
|
|
42
|
+
|
|
43
|
+
- **`dir/**` in-bounds touch glob is now recursive** — matches
|
|
44
|
+
`Dir.glob('**/**')` semantics; single-star `*` stays non-recursive.
|
|
45
|
+
Deep-globbed files inside a lane's declared touch set are in-bounds; outside
|
|
46
|
+
is still a spec defect (#52, #54).
|
|
47
|
+
- **`dispatched_at` recorded in `space.yaml` at dispatch time** (#18).
|
|
48
|
+
|
|
49
|
+
### Fixed
|
|
50
|
+
|
|
51
|
+
- **Canonical `section`/`freeze` commit message shape** — per-section commits
|
|
52
|
+
and the freeze commit carry the correct canonical prefix and body.
|
|
53
|
+
- **`space run --help` steers to the `--` separator** — a quoted multi-word
|
|
54
|
+
command otherwise arrives as one argv token → opaque in-guest failure; the
|
|
55
|
+
desc/argument/example now show the `--` form, with a subprocess test (#28).
|
|
56
|
+
- **Dropped a load-time "assigned but unused variable" warning** — `build_dir`
|
|
57
|
+
in `worktree_add` was computed but never read.
|
|
58
|
+
|
|
8
59
|
## [5.0.0] - 2026-07-06
|
|
9
60
|
|
|
10
61
|
### Added
|
|
@@ -20,13 +20,13 @@ module Space::Architect
|
|
|
20
20
|
FROZEN_BOUNDARY = /^## Builder Prompt/
|
|
21
21
|
|
|
22
22
|
# Sections the architect writes (and the CLI commits) via `architect section`.
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
# command (`architect evidence`) because it is transcribed verbatim from scratch.
|
|
23
|
+
# Builder Report has its own command (`architect evidence`) because it is
|
|
24
|
+
# transcribed verbatim from scratch.
|
|
26
25
|
# `frozen: true` sections live above the freeze boundary and are refused once frozen.
|
|
27
26
|
SECTIONS = {
|
|
28
27
|
"grounds" => { heading: "## Grounds", message: "grounds", prefix: "grounds", frozen: true },
|
|
29
28
|
"specification" => { heading: "## Specification", message: "specification", prefix: "spec", frozen: true },
|
|
29
|
+
"acceptance-criteria" => { heading: "## Acceptance Criteria", message: "acceptance criteria", prefix: "ac", frozen: true },
|
|
30
30
|
"prompt" => { heading: "## Builder Prompt", message: "dispatched", prefix: "prompt", frozen: false },
|
|
31
31
|
"verdict" => { heading: "## Verdict", message: "verdict", prefix: "verdict", frozen: false }
|
|
32
32
|
}.freeze
|
|
@@ -158,7 +158,8 @@ module Space::Architect
|
|
|
158
158
|
# Freeze the iteration: the iteration file must carry a "## Acceptance Criteria" section. Commits
|
|
159
159
|
# any pending changes to the iteration file and records HEAD as freeze_sha. If
|
|
160
160
|
# already frozen, refuses when the frozen region has changed since.
|
|
161
|
-
|
|
161
|
+
# With force: true, re-freezes a changed frozen region if no lane is dispatched yet.
|
|
162
|
+
def freeze!(iteration, warnings: nil, message: nil, force: false)
|
|
162
163
|
entry = slice_entry(iteration)
|
|
163
164
|
rel = entry["file"]
|
|
164
165
|
path = space.path.join(rel)
|
|
@@ -174,11 +175,17 @@ module Space::Architect
|
|
|
174
175
|
if entry["freeze_sha"]
|
|
175
176
|
sha = entry["freeze_sha"]
|
|
176
177
|
if frozen_region_changed?(sha, rel)
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
178
|
+
if force
|
|
179
|
+
dispatched_guard!(entry)
|
|
180
|
+
# fall through to commit path to re-freeze with new sha
|
|
181
|
+
else
|
|
182
|
+
raise Space::Core::Error,
|
|
183
|
+
"Frozen sections of #{rel} changed since freeze #{sha[0, 8]} — " \
|
|
184
|
+
"refusing to re-freeze. Restore them to their frozen state or use a new iteration."
|
|
185
|
+
end
|
|
186
|
+
else
|
|
187
|
+
return sha
|
|
180
188
|
end
|
|
181
|
-
return sha
|
|
182
189
|
end
|
|
183
190
|
|
|
184
191
|
files = [rel]
|
|
@@ -208,6 +215,9 @@ module Space::Architect
|
|
|
208
215
|
b
|
|
209
216
|
end
|
|
210
217
|
|
|
218
|
+
git_run("-C", space.path.to_s, "commit", "-m",
|
|
219
|
+
compose_message("I#{nn} freeze:", "I#{nn}: record freeze sha", message), "--", Space::Core::Space::METADATA_FILE)
|
|
220
|
+
|
|
211
221
|
sha
|
|
212
222
|
end
|
|
213
223
|
|
|
@@ -232,14 +242,15 @@ module Space::Architect
|
|
|
232
242
|
|
|
233
243
|
# Write one section of the iteration file and commit it with the canonical
|
|
234
244
|
# per-section message, in one call. Refuses to write a frozen section
|
|
235
|
-
# (Grounds/Specification) once the iteration is frozen.
|
|
236
|
-
#
|
|
237
|
-
|
|
245
|
+
# (Grounds/Specification/Acceptance Criteria) once the iteration is frozen.
|
|
246
|
+
# With force: true, writes a frozen section if no lane is dispatched yet.
|
|
247
|
+
# Builder Report is not here (use evidence).
|
|
248
|
+
def write_section!(iteration, section, body:, append: false, lane: nil, message: nil, force: false)
|
|
238
249
|
spec = SECTIONS[section]
|
|
239
250
|
unless spec
|
|
240
251
|
raise Space::Core::Error,
|
|
241
252
|
"Unknown section '#{section}' — one of: #{SECTIONS.keys.join(', ')}. " \
|
|
242
|
-
"(
|
|
253
|
+
"(Builder Report is written by `architect evidence`.)"
|
|
243
254
|
end
|
|
244
255
|
|
|
245
256
|
entry = slice_entry(iteration)
|
|
@@ -248,13 +259,19 @@ module Space::Architect
|
|
|
248
259
|
raise Space::Core::Error, "#{rel} does not exist — run `architect new #{iteration}` first" unless path.exist?
|
|
249
260
|
|
|
250
261
|
if spec[:frozen] && entry["freeze_sha"]
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
262
|
+
if force
|
|
263
|
+
dispatched_guard!(entry)
|
|
264
|
+
else
|
|
265
|
+
raise Space::Core::Error,
|
|
266
|
+
"#{spec[:heading]} is frozen for #{iteration} (freeze #{entry["freeze_sha"][0, 8]}) — " \
|
|
267
|
+
"frozen sections are read-only after the freeze commit. Open a new iteration to change the contract."
|
|
268
|
+
end
|
|
254
269
|
end
|
|
255
270
|
|
|
256
271
|
block = lane ? "### #{lane}\n\n#{body.strip}" : body.strip
|
|
257
|
-
|
|
272
|
+
new_text = replace_section_body(path.read, spec[:heading], block, append: append)
|
|
273
|
+
lint_gates!(new_text) if section == "acceptance-criteria"
|
|
274
|
+
path.write(new_text)
|
|
258
275
|
|
|
259
276
|
nn = format("%02d", entry["ordinal"] || 0)
|
|
260
277
|
_o, _e, cst = git_capture("-C", space.path.to_s, "commit", "-m",
|
|
@@ -342,13 +359,13 @@ module Space::Architect
|
|
|
342
359
|
# the lane branch, then merge --no-ff into the repo's lane/<id> integration branch.
|
|
343
360
|
# Runs NO gates and makes NO pass/fail decision. Refuses a mechanically-failing lane
|
|
344
361
|
# (builder commits / out-of-bounds) and aborts cleanly on a merge conflict.
|
|
345
|
-
def merge_lane!(iteration, lane, message: nil)
|
|
362
|
+
def merge_lane!(iteration, lane, message: nil, commit_mode: nil, into: nil)
|
|
346
363
|
entry = slice_entry(iteration)
|
|
347
364
|
lane_entry = (entry["lanes"] || []).find { |l| l["name"] == lane }
|
|
348
365
|
raise Space::Core::Error, "No lane '#{lane}' recorded for iteration '#{iteration}'" unless lane_entry
|
|
349
366
|
lane_entry = ensure_lane_materialized(iteration, lane)
|
|
350
367
|
|
|
351
|
-
checks = lane_mechanical_checks(entry, lane_entry)
|
|
368
|
+
checks = lane_mechanical_checks(entry, lane_entry, commit_mode: commit_mode)
|
|
352
369
|
if checks[:no_builder_commits] == false
|
|
353
370
|
raise Space::Core::Error, "Lane '#{lane}' has builder commits — the worktree is tampered (hard rule 7). Reset and re-dispatch; do not merge."
|
|
354
371
|
end
|
|
@@ -363,7 +380,7 @@ module Space::Architect
|
|
|
363
380
|
raise Space::Core::Error, "Worktree directory does not exist: #{wt_path}" unless wt_path.exist?
|
|
364
381
|
base_sha = lane_entry["base_sha"]
|
|
365
382
|
lane_branch = "lane/#{id}-#{lane}"
|
|
366
|
-
integration_branch = project_integration_branch
|
|
383
|
+
integration_branch = into || project_integration_branch
|
|
367
384
|
|
|
368
385
|
status_out, = git_capture("-C", wt_path.to_s, "status", "--porcelain")
|
|
369
386
|
raise Space::Core::Error, "Lane '#{lane}' worktree has no changes to integrate." if status_out.strip.empty?
|
|
@@ -385,9 +402,22 @@ module Space::Architect
|
|
|
385
402
|
unless mst.success?
|
|
386
403
|
conflicts, = git_capture("-C", repo_path.to_s, "diff", "--name-only", "--diff-filter=U")
|
|
387
404
|
git_capture("-C", repo_path.to_s, "merge", "--abort")
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
405
|
+
conflict_files = conflicts.split
|
|
406
|
+
lane_touch_set = lane_entry["touch_set"] || []
|
|
407
|
+
fnm = File::FNM_PATHNAME | File::FNM_EXTGLOB
|
|
408
|
+
outside = conflict_files.reject do |f|
|
|
409
|
+
lane_touch_set.any? { |g| File.fnmatch(g, f, fnm) || (g.end_with?("/**") && File.fnmatch("#{g}/*", f, fnm)) }
|
|
410
|
+
end
|
|
411
|
+
if !lane_touch_set.empty? && outside.empty?
|
|
412
|
+
raise Space::Core::Error,
|
|
413
|
+
"Merge conflict integrating lane '#{lane}' (#{conflict_files.join(", ")}) — the lane plan was " \
|
|
414
|
+
"not disjoint = a spec defect. Kill the conflicting lane and re-spec; do not hand-resolve. #{merr.strip}"
|
|
415
|
+
else
|
|
416
|
+
raise Space::Core::Error,
|
|
417
|
+
"Merge conflict integrating lane '#{lane}' (#{conflict_files.join(", ")}) — conflicting files " \
|
|
418
|
+
"are outside the lane's touch set; this looks like a branch mismatch: the lane is being merged " \
|
|
419
|
+
"into '#{integration_branch}'. Use --into <branch> to target the correct branch. #{merr.strip}"
|
|
420
|
+
end
|
|
391
421
|
end
|
|
392
422
|
|
|
393
423
|
merge_sha, = git_capture("-C", repo_path.to_s, "rev-parse", "HEAD")
|
|
@@ -414,14 +444,14 @@ module Space::Architect
|
|
|
414
444
|
# first conflict (a disjointness defect). Never decides which lanes pass. With no
|
|
415
445
|
# lanes and teardown: true, tears down every lane recorded for the iteration instead
|
|
416
446
|
# (the second, teardown-only call in the loop's integrate-then-teardown rhythm).
|
|
417
|
-
def integrate!(iteration, lanes: nil, teardown: false, message: nil)
|
|
447
|
+
def integrate!(iteration, lanes: nil, teardown: false, message: nil, commit_mode: nil, into: nil)
|
|
418
448
|
lanes = Array(lanes)
|
|
419
449
|
return teardown_lanes!(iteration, slice_entry(iteration)["lanes"] || []) if lanes.empty? && teardown
|
|
420
450
|
raise Space::Core::Error, "No lanes given to integrate" if lanes.empty?
|
|
421
451
|
|
|
422
452
|
merged = []
|
|
423
453
|
lanes.each do |lane|
|
|
424
|
-
merged << merge_lane!(iteration, lane, message: message)
|
|
454
|
+
merged << merge_lane!(iteration, lane, message: message, commit_mode: commit_mode, into: into)
|
|
425
455
|
rescue Space::Core::Error => e
|
|
426
456
|
done = merged.map { |m| m[:lane] }.join(", ")
|
|
427
457
|
raise Space::Core::Error, "Integrated #{done.empty? ? "(none)" : done} then stopped at '#{lane}': #{e.message}"
|
|
@@ -534,10 +564,46 @@ module Space::Architect
|
|
|
534
564
|
parts << "=== #{rel} ===\n\n#{iter_path.read}"
|
|
535
565
|
end
|
|
536
566
|
|
|
567
|
+
space.repos.each do |repo|
|
|
568
|
+
name = repo["name"]
|
|
569
|
+
repo_path = space.path.join("repos", name).to_s
|
|
570
|
+
next unless Dir.exist?(repo_path)
|
|
571
|
+
|
|
572
|
+
branch_out, _, branch_st = git_capture("-C", repo_path, "symbolic-ref", "--short", "HEAD")
|
|
573
|
+
next unless branch_st.success?
|
|
574
|
+
branch = branch_out.strip
|
|
575
|
+
|
|
576
|
+
git_capture("-C", repo_path, "fetch", "origin")
|
|
577
|
+
|
|
578
|
+
count_out, _, count_st = git_capture("-C", repo_path, "rev-list", "--left-right", "--count",
|
|
579
|
+
"#{branch}...origin/#{branch}")
|
|
580
|
+
next unless count_st.success?
|
|
581
|
+
|
|
582
|
+
behind = count_out.strip.split[1].to_i
|
|
583
|
+
if behind > 0
|
|
584
|
+
parts << "WARNING: repos/#{name} local #{branch} is #{behind} commits behind " \
|
|
585
|
+
"origin/#{branch} — run `architect sync #{name}`"
|
|
586
|
+
end
|
|
587
|
+
rescue
|
|
588
|
+
# tolerate fetch or comparison failures silently
|
|
589
|
+
end
|
|
590
|
+
|
|
537
591
|
parts.join("\n")
|
|
538
592
|
end
|
|
539
593
|
|
|
540
|
-
|
|
594
|
+
# Sync tracked repo clones with their remotes (fast-forward only).
|
|
595
|
+
# Returns an array of result hashes: { repo:, status:, message: }.
|
|
596
|
+
# With no repo_name, syncs every tracked repo; with a name, syncs only that one.
|
|
597
|
+
def sync_repos(repo_name: nil)
|
|
598
|
+
repos = space.repos
|
|
599
|
+
if repo_name
|
|
600
|
+
repos = repos.select { |r| r["name"] == repo_name }
|
|
601
|
+
raise Space::Core::Error, "repo '#{repo_name}' not tracked in this space" if repos.empty?
|
|
602
|
+
end
|
|
603
|
+
repos.map { |r| sync_one_repo(r["name"]) }
|
|
604
|
+
end
|
|
605
|
+
|
|
606
|
+
def worktree_add(repo, iteration, lane, base: nil, harness: "claude-code", model: nil, variant: false, effort: nil, touch: nil, force: false)
|
|
541
607
|
if harness.to_s == "opencode" && (model.nil? || model == Harness::CLAUDE_DEFAULT_MODEL)
|
|
542
608
|
raise Space::Core::Error,
|
|
543
609
|
"Pass --model when using --harness opencode " \
|
|
@@ -556,7 +622,6 @@ module Space::Architect
|
|
|
556
622
|
|
|
557
623
|
id = iteration_id(entry)
|
|
558
624
|
wt_path = space.path.join("build", "#{id}-#{lane}", "wt")
|
|
559
|
-
build_dir = space.path.join("build", "#{id}-#{lane}")
|
|
560
625
|
FileUtils.mkdir_p(wt_path.dirname)
|
|
561
626
|
|
|
562
627
|
base_ref = base || "HEAD"
|
|
@@ -566,11 +631,15 @@ module Space::Architect
|
|
|
566
631
|
|
|
567
632
|
branch = "lane/#{id}-#{lane}"
|
|
568
633
|
|
|
569
|
-
# Guard: an existing directory that is not a registered worktree is ambiguous
|
|
634
|
+
# Guard: an existing directory that is not a registered worktree is ambiguous.
|
|
570
635
|
if wt_path.exist? && !worktree_registered?(repo_path, wt_path)
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
636
|
+
if force
|
|
637
|
+
FileUtils.rm_rf(wt_path)
|
|
638
|
+
else
|
|
639
|
+
raise Space::Core::Error,
|
|
640
|
+
"#{wt_path} exists but is not a registered git worktree of #{repo} — " \
|
|
641
|
+
"resolve manually before re-running worktree_add, or re-run with --force to clear and re-create it"
|
|
642
|
+
end
|
|
574
643
|
end
|
|
575
644
|
|
|
576
645
|
# Skip git worktree add when the branch and worktree already exist (idempotent re-run).
|
|
@@ -727,7 +796,7 @@ module Space::Architect
|
|
|
727
796
|
# record worktree/base_sha/integration_branch. Idempotent — an already-materialized
|
|
728
797
|
# lane is skipped, not re-created. Refuses until the iteration is frozen, because
|
|
729
798
|
# declarations are not authoritative until then.
|
|
730
|
-
def provision(iteration, base: nil, lane: nil)
|
|
799
|
+
def provision(iteration, base: nil, lane: nil, force: false)
|
|
731
800
|
entry = slice_entry(iteration)
|
|
732
801
|
raise Space::Core::Error,
|
|
733
802
|
"Iteration '#{iteration}' is not frozen — freeze before provisioning (declarations are not authoritative until frozen)." \
|
|
@@ -745,17 +814,17 @@ module Space::Architect
|
|
|
745
814
|
{ lane: name, worktree: wt_path, base_sha: l["base_sha"], created: false }
|
|
746
815
|
else
|
|
747
816
|
result = worktree_add(l["repo"], iteration, name, base: resolve_lane_base(l["repo"], base),
|
|
748
|
-
**recorded_lane_fields(l))
|
|
817
|
+
force: force, **recorded_lane_fields(l))
|
|
749
818
|
{ lane: name, worktree: result[:worktree], base_sha: result[:base_sha], created: true }
|
|
750
819
|
end
|
|
751
820
|
end
|
|
752
821
|
end
|
|
753
822
|
|
|
754
|
-
def verify(iteration)
|
|
823
|
+
def verify(iteration, commit_mode: nil)
|
|
755
824
|
entry = slice_entry(iteration)
|
|
756
825
|
(entry["lanes"] || []).map do |lane|
|
|
757
826
|
ensure_lane_materialized(iteration, lane["name"])
|
|
758
|
-
{ lane: lane["name"], repo: lane["repo"], checks: lane_mechanical_checks(entry, lane) }
|
|
827
|
+
{ lane: lane["name"], repo: lane["repo"], checks: lane_mechanical_checks(entry, lane, commit_mode: commit_mode) }
|
|
759
828
|
end
|
|
760
829
|
end
|
|
761
830
|
|
|
@@ -1030,7 +1099,7 @@ module Space::Architect
|
|
|
1030
1099
|
|
|
1031
1100
|
# The four per-lane post-flight checks, shared by `verify` (reports) and
|
|
1032
1101
|
# `merge_lane!` (refuses on failure) so the two can never drift.
|
|
1033
|
-
def lane_mechanical_checks(entry, lane)
|
|
1102
|
+
def lane_mechanical_checks(entry, lane, commit_mode: nil)
|
|
1034
1103
|
freeze_sha = entry["freeze_sha"]
|
|
1035
1104
|
rel = entry["file"]
|
|
1036
1105
|
lane_name = lane["name"]
|
|
@@ -1043,12 +1112,21 @@ module Space::Architect
|
|
|
1043
1112
|
# (a) frozen sections of the iteration file untouched since freeze
|
|
1044
1113
|
checks[:frozen_untouched] = (!frozen_region_changed?(freeze_sha, rel) if freeze_sha && rel)
|
|
1045
1114
|
|
|
1046
|
-
# (b) no builder commits in the worktree (the architect's integrate commit is excluded
|
|
1047
|
-
|
|
1048
|
-
|
|
1115
|
+
# (b) no builder commits in the worktree (the architect's integrate commit is excluded;
|
|
1116
|
+
# in conductor mode, canonical conductor commits are also excluded)
|
|
1117
|
+
effective_commit_mode = commit_mode || space.data.dig("project", "commit_mode") || "strict"
|
|
1118
|
+
log_out, = git_capture("-C", wt_path.to_s, "log", "--format=%H%x09%s", "#{base_sha}..")
|
|
1119
|
+
commit_entries = log_out.strip.split("\n").filter_map do |line|
|
|
1120
|
+
sha, subject = line.strip.split("\t", 2)
|
|
1121
|
+
{ sha: sha, subject: subject.to_s } unless sha.nil? || sha.empty?
|
|
1122
|
+
end
|
|
1049
1123
|
recorded_integrate = lane["integrate_sha"]&.strip
|
|
1050
|
-
|
|
1051
|
-
|
|
1124
|
+
canonical_conductor = "#{iteration_id(entry)}-#{lane_name}: builder output"
|
|
1125
|
+
builder_commits = commit_entries.reject do |c|
|
|
1126
|
+
c[:sha] == recorded_integrate ||
|
|
1127
|
+
(effective_commit_mode == "conductor" && c[:subject] == canonical_conductor)
|
|
1128
|
+
end
|
|
1129
|
+
checks[:no_builder_commits] = builder_commits.empty?
|
|
1052
1130
|
|
|
1053
1131
|
# (c) builder's scratch report exists and is non-empty
|
|
1054
1132
|
report = space.path.join("build", "#{iteration_id(entry)}-#{lane_name}", "report.md")
|
|
@@ -1059,7 +1137,7 @@ module Space::Architect
|
|
|
1059
1137
|
:no_touch_set
|
|
1060
1138
|
else
|
|
1061
1139
|
# -z: NUL-delimited; renames emit new_path NUL old_path — include both
|
|
1062
|
-
status_out, = git_capture("-C", wt_path.to_s, "status", "--porcelain", "-z")
|
|
1140
|
+
status_out, = git_capture("-C", wt_path.to_s, "status", "--porcelain", "-z", "-uall")
|
|
1063
1141
|
changed = []
|
|
1064
1142
|
entries = status_out.split("\0")
|
|
1065
1143
|
i = 0
|
|
@@ -1076,7 +1154,12 @@ module Space::Architect
|
|
|
1076
1154
|
changed << orig if orig && !orig.empty?
|
|
1077
1155
|
end
|
|
1078
1156
|
fnm = File::FNM_PATHNAME | File::FNM_EXTGLOB
|
|
1079
|
-
changed.all?
|
|
1157
|
+
changed.all? do |f|
|
|
1158
|
+
touch_set.any? do |g|
|
|
1159
|
+
File.fnmatch(g, f, fnm) ||
|
|
1160
|
+
(g.end_with?("/**") && File.fnmatch("#{g}/*", f, fnm))
|
|
1161
|
+
end
|
|
1162
|
+
end
|
|
1080
1163
|
end
|
|
1081
1164
|
|
|
1082
1165
|
checks
|
|
@@ -1190,6 +1273,17 @@ module Space::Architect
|
|
|
1190
1273
|
raise Space::Core::Error, "ill-formed gates block:\n#{result.failure.join("\n")}"
|
|
1191
1274
|
end
|
|
1192
1275
|
|
|
1276
|
+
# Raises if any lane in the entry has been dispatched (dispatched_at or integrate_sha set).
|
|
1277
|
+
# Used by freeze! and write_section! --force to prevent rewriting frozen content after a
|
|
1278
|
+
# builder has run (moving freeze_sha post-dispatch breaks the AC cardinal invariant).
|
|
1279
|
+
def dispatched_guard!(entry)
|
|
1280
|
+
lane = (entry["lanes"] || []).find { |l| l["dispatched_at"] || l["integrate_sha"] }
|
|
1281
|
+
return unless lane
|
|
1282
|
+
raise Space::Core::Error,
|
|
1283
|
+
"Lane '#{lane["name"]}' is already dispatched — cannot re-freeze or write frozen sections " \
|
|
1284
|
+
"after dispatch (a builder has run against the frozen AC; rewriting it breaks the cardinal invariant)."
|
|
1285
|
+
end
|
|
1286
|
+
|
|
1193
1287
|
def staged_changes?
|
|
1194
1288
|
_o, _e, st = git_capture("-C", space.path.to_s, "diff", "--cached", "--quiet")
|
|
1195
1289
|
!st.success? # --quiet exits non-zero when there are staged differences
|
|
@@ -1235,6 +1329,45 @@ module Space::Architect
|
|
|
1235
1329
|
Open3.capture3("git", *args)
|
|
1236
1330
|
end
|
|
1237
1331
|
|
|
1332
|
+
def sync_one_repo(name)
|
|
1333
|
+
repo_path = space.path.join("repos", name).to_s
|
|
1334
|
+
|
|
1335
|
+
dirty_out, _, _ = git_capture("-C", repo_path, "status", "--porcelain")
|
|
1336
|
+
return { repo: name, status: :dirty, message: "#{name}: dirty working tree — skipping" } if dirty_out.strip.length > 0
|
|
1337
|
+
|
|
1338
|
+
branch_out, _, branch_st = git_capture("-C", repo_path, "symbolic-ref", "--short", "HEAD")
|
|
1339
|
+
unless branch_st.success?
|
|
1340
|
+
return { repo: name, status: :error, message: "#{name}: detached HEAD — skipping" }
|
|
1341
|
+
end
|
|
1342
|
+
branch = branch_out.strip
|
|
1343
|
+
|
|
1344
|
+
_, fetch_err, fetch_st = git_capture("-C", repo_path, "fetch", "origin")
|
|
1345
|
+
unless fetch_st.success?
|
|
1346
|
+
return { repo: name, status: :error, message: "#{name}: fetch failed — #{fetch_err.strip}" }
|
|
1347
|
+
end
|
|
1348
|
+
|
|
1349
|
+
count_out, _, count_st = git_capture("-C", repo_path, "rev-list", "--left-right", "--count",
|
|
1350
|
+
"#{branch}...origin/#{branch}")
|
|
1351
|
+
unless count_st.success?
|
|
1352
|
+
return { repo: name, status: :error, message: "#{name}: could not compare with origin/#{branch}" }
|
|
1353
|
+
end
|
|
1354
|
+
|
|
1355
|
+
ahead, behind = count_out.strip.split.map(&:to_i)
|
|
1356
|
+
return { repo: name, status: :up_to_date, message: "#{name}: up to date" } if behind == 0
|
|
1357
|
+
|
|
1358
|
+
if ahead > 0
|
|
1359
|
+
return { repo: name, status: :diverged,
|
|
1360
|
+
message: "#{name}: behind #{behind}, diverged #{ahead} — not fast-forwardable, resolve manually" }
|
|
1361
|
+
end
|
|
1362
|
+
|
|
1363
|
+
_, ff_err, ff_st = git_capture("-C", repo_path, "merge", "--ff-only", "origin/#{branch}")
|
|
1364
|
+
if ff_st.success?
|
|
1365
|
+
{ repo: name, status: :fast_forwarded, message: "#{name}: fast-forwarded #{behind} commits" }
|
|
1366
|
+
else
|
|
1367
|
+
{ repo: name, status: :ff_failed, message: "#{name}: merge --ff-only failed — #{ff_err.strip}" }
|
|
1368
|
+
end
|
|
1369
|
+
end
|
|
1370
|
+
|
|
1238
1371
|
def branch_exists?(repo_path, branch)
|
|
1239
1372
|
_, _, st = git_capture("-C", repo_path.to_s, "rev-parse", "--verify", branch)
|
|
1240
1373
|
st.success?
|
|
@@ -174,20 +174,40 @@ module Space::Architect
|
|
|
174
174
|
end
|
|
175
175
|
end
|
|
176
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
|
+
|
|
177
196
|
class Freeze < BaseCommand
|
|
178
197
|
desc "Freeze the iteration's frozen region (Grounds/Specification/Acceptance Criteria) and record the freeze SHA"
|
|
179
198
|
phase 12, "Spec"
|
|
180
199
|
argument :iteration, required: true, desc: "Iteration name"
|
|
181
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)"
|
|
182
202
|
commit_message_options
|
|
183
203
|
|
|
184
|
-
def call(iteration:, space: nil, message: nil, message_from: nil, **opts)
|
|
204
|
+
def call(iteration:, space: nil, message: nil, message_from: nil, force: false, **opts)
|
|
185
205
|
setup_terminal(**opts.slice(:color, :colors))
|
|
186
206
|
handle_errors do
|
|
187
207
|
render(store.find(space)) do |sp|
|
|
188
208
|
project = ArchitectProject.new(space: sp)
|
|
189
209
|
warnings = []
|
|
190
|
-
sha = project.freeze!(iteration, warnings: warnings,
|
|
210
|
+
sha = project.freeze!(iteration, warnings: warnings, force: force,
|
|
191
211
|
message: read_commit_message(message: message, message_from: message_from))
|
|
192
212
|
terminal.say "Frozen #{iteration} at #{sha}"
|
|
193
213
|
warnings.each { |w| terminal.say "Warning: #{w}" }
|
|
@@ -206,15 +226,17 @@ module Space::Architect
|
|
|
206
226
|
class Verify < BaseCommand
|
|
207
227
|
desc "Post-flight mechanical lane checks — frozen-untouched, no builder commits, report exists, in-bounds (reports only, no judgment)"
|
|
208
228
|
phase 30, "Judge"
|
|
209
|
-
argument :iteration,
|
|
210
|
-
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"
|
|
211
232
|
|
|
212
|
-
def call(iteration:, space: nil, **opts)
|
|
233
|
+
def call(iteration:, space: nil, commit_mode: nil, **opts)
|
|
213
234
|
setup_terminal(**opts.slice(:color, :colors))
|
|
214
235
|
handle_errors do
|
|
215
236
|
render(store.find(space)) do |sp|
|
|
216
237
|
project = ArchitectProject.new(space: sp)
|
|
217
|
-
|
|
238
|
+
terminal.say "Effective commit_mode: #{commit_mode}" if commit_mode
|
|
239
|
+
results = project.verify(iteration, commit_mode: commit_mode)
|
|
218
240
|
|
|
219
241
|
if results.empty?
|
|
220
242
|
terminal.say "No lanes recorded for iteration '#{iteration}'"
|
|
@@ -316,13 +338,14 @@ module Space::Architect
|
|
|
316
338
|
argument :space, required: false, desc: "Space identifier (default: $PWD)"
|
|
317
339
|
option :base, default: nil, desc: "Base ref override (default: project/<slug> HEAD if it exists, else the repo's default branch)"
|
|
318
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"
|
|
319
342
|
|
|
320
|
-
def call(iteration:, space: nil, base: nil, lane: nil, **opts)
|
|
343
|
+
def call(iteration:, space: nil, base: nil, lane: nil, force: false, **opts)
|
|
321
344
|
setup_terminal(**opts.slice(:color, :colors))
|
|
322
345
|
handle_errors do
|
|
323
346
|
render(store.find(space)) do |sp|
|
|
324
347
|
project = ArchitectProject.new(space: sp)
|
|
325
|
-
results = project.provision(iteration, base: base, lane: lane)
|
|
348
|
+
results = project.provision(iteration, base: base, lane: lane, force: force)
|
|
326
349
|
if results.empty?
|
|
327
350
|
terminal.say "No declared lanes to provision for '#{iteration}'"
|
|
328
351
|
else
|
|
@@ -341,23 +364,24 @@ module Space::Architect
|
|
|
341
364
|
desc "Write a section of the iteration file and commit it (one call)"
|
|
342
365
|
phase 11, "Spec"
|
|
343
366
|
argument :iteration, required: true, desc: "Iteration name"
|
|
344
|
-
argument :section, required: true, desc: "Section: grounds, specification, prompt, verdict"
|
|
367
|
+
argument :section, required: true, desc: "Section: grounds, specification, acceptance-criteria, prompt, verdict"
|
|
345
368
|
argument :space, required: false, desc: "Space identifier (default: $PWD)"
|
|
346
369
|
option :from, default: nil, desc: "Read the section body from this file"
|
|
347
370
|
option :body, default: nil, desc: "Inline section body (one-liners)"
|
|
348
371
|
option :stdin, type: :boolean, default: false, desc: "Read the section body from stdin"
|
|
349
372
|
option :append, type: :boolean, default: false, desc: "Append a ### <lane> subsection instead of replacing"
|
|
350
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)"
|
|
351
375
|
commit_message_options
|
|
352
376
|
|
|
353
377
|
def call(iteration:, section:, space: nil, from: nil, body: nil, stdin: false, append: false, lane: nil,
|
|
354
|
-
message: nil, message_from: nil, **opts)
|
|
378
|
+
message: nil, message_from: nil, force: false, **opts)
|
|
355
379
|
setup_terminal(**opts.slice(:color, :colors))
|
|
356
380
|
handle_errors do
|
|
357
381
|
content = read_body(from: from, body: body, stdin: stdin, what: "section body")
|
|
358
382
|
render(store.find(space)) do |sp|
|
|
359
383
|
project = ArchitectProject.new(space: sp)
|
|
360
|
-
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,
|
|
361
385
|
message: read_commit_message(message: message, message_from: message_from))
|
|
362
386
|
if res[:committed]
|
|
363
387
|
terminal.say "Committed #{res[:heading]} → #{res[:sha][0, 8]}"
|
|
@@ -425,17 +449,19 @@ module Space::Architect
|
|
|
425
449
|
class Merge < BaseCommand
|
|
426
450
|
desc "Integrate ONE judged-passing lane (merges --no-ff; runs no gates, makes no verdict)"
|
|
427
451
|
phase 41, "Land"
|
|
428
|
-
argument :iteration,
|
|
429
|
-
argument :lane,
|
|
430
|
-
argument :space,
|
|
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"
|
|
431
457
|
commit_message_options
|
|
432
458
|
|
|
433
|
-
def call(iteration:, lane:, space: nil, message: nil, message_from: nil, **opts)
|
|
459
|
+
def call(iteration:, lane:, space: nil, message: nil, message_from: nil, into: nil, commit_mode: nil, **opts)
|
|
434
460
|
setup_terminal(**opts.slice(:color, :colors))
|
|
435
461
|
handle_errors do
|
|
436
462
|
render(store.find(space)) do |sp|
|
|
437
463
|
project = ArchitectProject.new(space: sp)
|
|
438
|
-
r = project.merge_lane!(iteration, lane,
|
|
464
|
+
r = project.merge_lane!(iteration, lane, into: into, commit_mode: commit_mode,
|
|
439
465
|
message: read_commit_message(message: message, message_from: message_from))
|
|
440
466
|
terminal.say "Merged #{lane} → #{r[:integration_branch]} (#{r[:merge_sha][0, 8]})"
|
|
441
467
|
terminal.say r[:diffstat] unless r[:diffstat].empty?
|
|
@@ -449,13 +475,15 @@ module Space::Architect
|
|
|
449
475
|
class Integrate < BaseCommand
|
|
450
476
|
desc "Integrate the architect-supplied set of passing lanes, in order (stops on conflict)"
|
|
451
477
|
phase 40, "Land"
|
|
452
|
-
argument :iteration,
|
|
453
|
-
argument :space,
|
|
454
|
-
option :lanes,
|
|
455
|
-
option :teardown,
|
|
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"
|
|
456
484
|
commit_message_options
|
|
457
485
|
|
|
458
|
-
def call(iteration:, space: nil, lanes: nil, teardown: false, message: nil, message_from: nil, **opts)
|
|
486
|
+
def call(iteration:, space: nil, lanes: nil, teardown: false, message: nil, message_from: nil, commit_mode: nil, into: nil, **opts)
|
|
459
487
|
setup_terminal(**opts.slice(:color, :colors))
|
|
460
488
|
handle_errors do
|
|
461
489
|
lane_names = lanes.to_s.split(",").map(&:strip).reject(&:empty?)
|
|
@@ -465,7 +493,8 @@ module Space::Architect
|
|
|
465
493
|
render(store.find(space)) do |sp|
|
|
466
494
|
project = ArchitectProject.new(space: sp)
|
|
467
495
|
results = project.integrate!(iteration, lanes: lane_names, teardown: teardown,
|
|
468
|
-
message: read_commit_message(message: message, message_from: message_from)
|
|
496
|
+
message: read_commit_message(message: message, message_from: message_from),
|
|
497
|
+
commit_mode: commit_mode, into: into)
|
|
469
498
|
if lane_names.empty?
|
|
470
499
|
if results.empty?
|
|
471
500
|
terminal.say "Nothing to tear down for #{iteration}"
|
|
@@ -574,15 +603,16 @@ module Space::Architect
|
|
|
574
603
|
option :model, default: nil, desc: "Model (required for opencode)"
|
|
575
604
|
option :effort, default: nil, desc: "Reasoning effort (opencode only; sets reasoningEffort in the model config)"
|
|
576
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"
|
|
577
607
|
|
|
578
|
-
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)
|
|
579
609
|
setup_terminal(**opts.slice(:color, :colors))
|
|
580
610
|
handle_errors do
|
|
581
611
|
render(store.find) do |sp|
|
|
582
612
|
project = ArchitectProject.new(space: sp)
|
|
583
613
|
touch_set = touch ? touch.split(",").map(&:strip).reject(&:empty?) : nil
|
|
584
614
|
result = project.worktree_add(repo, iteration, lane, base: base,
|
|
585
|
-
harness: harness, model: model, effort: effort, touch: touch_set)
|
|
615
|
+
harness: harness, model: model, effort: effort, touch: touch_set, force: force)
|
|
586
616
|
terminal.say "Worktree: #{terminal.path(result[:worktree])}"
|
|
587
617
|
terminal.say "Base SHA: #{result[:base_sha]}"
|
|
588
618
|
CLI.record_outcome(Outcome.new(exit_code: 0))
|
|
@@ -756,6 +786,7 @@ Space::Architect::CLI::Registry.register "init", Space::Architect::CLI::Archit
|
|
|
756
786
|
Space::Architect::CLI::Registry.register "ground", Space::Architect::CLI::Architect::Ground
|
|
757
787
|
Space::Architect::CLI::Registry.register "new", Space::Architect::CLI::Architect::New
|
|
758
788
|
Space::Architect::CLI::Registry.register "status", Space::Architect::CLI::Architect::Status
|
|
789
|
+
Space::Architect::CLI::Registry.register "sync", Space::Architect::CLI::Architect::Sync
|
|
759
790
|
Space::Architect::CLI::Registry.register "freeze", Space::Architect::CLI::Architect::Freeze
|
|
760
791
|
Space::Architect::CLI::Registry.register "verify", Space::Architect::CLI::Architect::Verify
|
|
761
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
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: 5.
|
|
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
|