space-architect 9.1.0 → 9.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c2dfa94f98278929ea382e4411856749de3af270511f60760ee4e3bed83daf53
4
- data.tar.gz: 33c9bf994eafc772c67399876826b77f685a7ac9e3417f38b37054d853b5479b
3
+ metadata.gz: 154e74a35cc7d14274f2f6fec65f40400727adbf3ce4bcf69000c2f00405a07e
4
+ data.tar.gz: 2d6407b32a61f8c056e866e12dceab65b6a0b35f281ec6b1721c73233517da49
5
5
  SHA512:
6
- metadata.gz: 2b7c099ff9ffc9b1cf6e37d48a687b2c08c4f2c0463ea5f1e77d156f214bdf9e4f5d0059e1be9fa1247de440c50ea091ffb933db5f02c3bb8e2d8d75ed0f3996
7
- data.tar.gz: 5ce698b9ed448dbab61db46302c5f51f4e0bd850c8748c052131ffea4ef718326a3a4de47562092ba782dcfcfa042c6dedfe1c56675119b9ba2d98e29caf5f49
6
+ metadata.gz: db472c7ffcff0c4228f9c6bda1baad0c00cb0283defa3a686924267d51d252b9f24e3cdf65a8d50076f0354f5af1894ff6ff9799c2b783069a8bf52567206c2b
7
+ data.tar.gz: 444e06a1bbcbefe3f827a065cefba6383d04606215abf0ab016069f7b254a8c61d2343aa91a648a3a2be92ae1a47c97b18dbcdc63bb62a92d23c63b3b8b10e6f
@@ -929,12 +929,21 @@ module Space::Architect
929
929
  base_explicit = !base.nil?
930
930
  refuse_repoint_if_dirty!(iteration, lanes, base, iteration_id(entry)) if base_explicit && !force
931
931
 
932
+ # Sync-or-refuse ONCE per distinct repo of the selected set (lane order,
933
+ # first occurrence) BEFORE any worktree is cut — a divergence aborts the
934
+ # entire provision. An explicit --base is the deliberate case (#88): the
935
+ # pin is cut exactly as given, no sync, no divergence refusal.
936
+ synced = {}
937
+ unless base_explicit
938
+ lanes.map { |l| l["repo"] }.uniq.each { |repo| synced[repo] = sync_integration_branch!(repo, iteration) }
939
+ end
940
+
932
941
  lanes.map do |l|
933
942
  name = l["name"]
934
943
  result = worktree_add(l["repo"], iteration, name, base: resolve_lane_base(l["repo"], base),
935
944
  base_explicit: base_explicit, force: force, **recorded_lane_fields(l))
936
945
  { lane: name, worktree: result[:worktree], base_sha: result[:base_sha], outcome: result[:outcome],
937
- discarded: result[:discarded] }
946
+ discarded: result[:discarded], synced: synced[l["repo"]] }
938
947
  end
939
948
  end
940
949
 
@@ -1622,6 +1631,15 @@ module Space::Architect
1622
1631
  wt_path = space.path.join(lane_entry["worktree"] || "build/#{iteration_id(entry)}-#{lane}/wt")
1623
1632
  return lane_entry if wt_path.exist? && worktree_registered?(repo_path, wt_path)
1624
1633
 
1634
+ # Same sync-or-refuse as provision, immediately before the cut — a
1635
+ # dispatch/gate/integrate/verify that materializes a missing worktree must
1636
+ # never cut it from a stale integration tip. Already-materialized lanes
1637
+ # returned above and stay a true no-op. The inform goes on $stderr, like
1638
+ # worktree_add's own informs on this path; no-op syncs print nothing.
1639
+ report = sync_integration_branch!(lane_entry["repo"], iteration)
1640
+ $stderr.puts "#{report[:repo]}: fast-forwarded #{report[:branch]} to #{report[:sha][0, 8]} — " \
1641
+ "synced before materializing #{iteration}/#{lane}" if report
1642
+
1625
1643
  worktree_add(lane_entry["repo"], iteration, lane, base: resolve_lane_base(lane_entry["repo"], nil),
1626
1644
  base_explicit: false, **recorded_lane_fields(lane_entry))
1627
1645
  (slice_entry(iteration)["lanes"] || []).find { |l| l["name"] == lane }
@@ -1735,6 +1753,57 @@ module Space::Architect
1735
1753
  st.success? && !out.strip.empty? ? out.strip : "HEAD"
1736
1754
  end
1737
1755
 
1756
+ # Fast-forward the repo's project integration branch from its default branch
1757
+ # BEFORE any lane worktree is cut from it (I06 provision-sync — the I05 burn:
1758
+ # a worktree cut from a stale project/<slug> tip). Local-to-local only — no
1759
+ # fetch/pull, no remote-tracking ref is read or written. Compare before moving:
1760
+ # default not ahead → no-op (nil, callers print nothing); default ahead and
1761
+ # integration not ahead → fast-forward; both ahead → refuse (a human merges
1762
+ # deliberately, or pins --base) — --force has no bypass. Bang: it raises.
1763
+ # Returns nil for a no-op, or { repo:, branch:, sha: } for the moved ref so
1764
+ # callers can report the movement (never silently move a branch).
1765
+ def sync_integration_branch!(repo, iteration)
1766
+ repo_path = space.path.join("repos", repo)
1767
+ integration = project_integration_branch
1768
+ return nil unless branch_exists?(repo_path, integration)
1769
+
1770
+ default = repo_default_branch(repo_path)
1771
+ counts_out, counts_err, counts_st = git_capture("-C", repo_path.to_s, "rev-list", "--left-right", "--count",
1772
+ "#{default}...#{integration}")
1773
+ unless counts_st.success?
1774
+ raise Space::Core::Error, "Cannot compare #{default}...#{integration} in repos/#{repo} — #{counts_err.strip}"
1775
+ end
1776
+
1777
+ default_ahead, integration_ahead = counts_out.strip.split.map(&:to_i)
1778
+ return nil if default_ahead.zero?
1779
+
1780
+ default_tip, = git_capture("-C", repo_path.to_s, "rev-parse", default)
1781
+ integration_tip, = git_capture("-C", repo_path.to_s, "rev-parse", integration)
1782
+ if integration_ahead.positive?
1783
+ raise Space::Core::Error,
1784
+ "Refusing to provision '#{iteration}' — repos/#{repo} has diverged: #{default} (#{default_tip[0, 8]}) and " \
1785
+ "#{integration} (#{integration_tip[0, 8]}) each hold commits the other lacks " \
1786
+ "(#{default} ahead by #{default_ahead}, #{integration} ahead by #{integration_ahead}) — not fast-forwardable. " \
1787
+ "A human must merge #{default} into #{integration} deliberately, or pin the base explicitly with " \
1788
+ "`architect provision #{iteration} --base <sha>`. --force does not bypass this refusal."
1789
+ end
1790
+
1791
+ head_out, _, head_st = git_capture("-C", repo_path.to_s, "symbolic-ref", "--short", "HEAD")
1792
+ if head_st.success? && head_out.strip == integration
1793
+ # The integration branch is the clone's checked-out branch (the real
1794
+ # post-integrate state — merge_lane! checks it out) — fetch refuses a
1795
+ # checked-out dst, so fast-forward via merge instead.
1796
+ git_run("-C", repo_path.to_s, "merge", "--ff-only", default)
1797
+ else
1798
+ # Unprefixed refspecs are ff-only by construction — git rejects a non-ff
1799
+ # update here, so the branch can only ever move forward.
1800
+ git_run("-C", repo_path.to_s, "fetch", ".", "#{default}:#{integration}")
1801
+ end
1802
+
1803
+ new_tip, = git_capture("-C", repo_path.to_s, "rev-parse", integration)
1804
+ { repo: repo, branch: integration, sha: new_tip.strip }
1805
+ end
1806
+
1738
1807
  def slice_entry(iteration)
1739
1808
  block = space.data["project"] || {}
1740
1809
  entry = (block["iterations"] || []).find { |s| s["name"] == iteration }
@@ -562,7 +562,7 @@ module Space::Architect
562
562
  phase 20, "Build"
563
563
  argument :iteration, required: true, desc: "Iteration name"
564
564
  argument :space, required: false, desc: "Space identifier (default: $PWD)"
565
- option :base, default: nil, desc: "Base ref override (default: project/<slug> HEAD if it exists, else the repo's default branch)"
565
+ option :base, default: nil, desc: "Base ref override (default: project/<slug> fast-forward-synced from the repo's default branch first — diverged → refuse; --base skips the sync and cuts exactly the given ref)"
566
566
  option :lane, default: nil, desc: "Provision only this lane (default: all declared lanes)"
567
567
  option :force, type: :boolean, default: false, desc: "Clear and re-create a stale (unregistered) worktree directory"
568
568
 
@@ -575,6 +575,12 @@ module Space::Architect
575
575
  if results.empty?
576
576
  terminal.say "No declared lanes to provision for '#{iteration}'"
577
577
  else
578
+ # Sync report first (the sync ran before any cut): one line per
579
+ # moved branch — repo, branch, new short tip. No-op syncs are
580
+ # nil here and print nothing.
581
+ results.filter_map { |r| r[:synced] }.uniq.each do |s|
582
+ terminal.say "#{s[:repo]}: fast-forwarded #{s[:branch]} to #{s[:sha][0, 8]}"
583
+ end
578
584
  results.each do |r|
579
585
  state = case r[:outcome]
580
586
  when :created then "created"
@@ -17,9 +17,9 @@ module Space::Architect
17
17
  # XDG_CONFIG_HOME); space-local is architect.yaml beside the loaded
18
18
  # space.yaml (the CLI already resolved the space — no walk-up).
19
19
  module Routing
20
- ROLES = %w[architect spec builder judge coordinator].freeze
21
- # The four agent roles that must be routed after the merge; `architect` is optional.
22
- REQUIRED_ROLES = %w[spec builder judge coordinator].freeze
20
+ ROLES = %w[architect spec builder judge coordinator narrator].freeze
21
+ # The five agent roles that must be routed after the merge; `architect` is optional.
22
+ REQUIRED_ROLES = %w[spec builder judge coordinator narrator].freeze
23
23
  ENTRY_KEYS = %w[model thinking].freeze
24
24
 
25
25
  def self.global_path
@@ -34,7 +34,7 @@ module Space::Architect
34
34
 
35
35
  # Parse one file's text into { role => {"model" => id, "thinking" => level|nil} }.
36
36
  # +path+ is used only in error messages. Frozen v1 rules: integer version 1,
37
- # the five known roles, entries ⊆ {model, thinking} with a non-empty model,
37
+ # the six known roles, entries ⊆ {model, thinking} with a non-empty model,
38
38
  # duplicate mapping keys rejected (psych's plain load silently keeps the
39
39
  # last — the npm yaml the extension parses with throws), aliases accepted,
40
40
  # a trailing :level suffix on model parsed via Harness.parse_model_suffix
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Space
4
4
  module Architect
5
- VERSION = "9.1.0"
5
+ VERSION = "9.1.1"
6
6
  end
7
7
  end
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: 9.1.0
4
+ version: 9.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Eric Jacobs