space-architect 1.3.0 → 2.0.0.rc2

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.
Files changed (130) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +103 -0
  3. data/README.md +267 -158
  4. data/exe/architect +1 -1
  5. data/exe/space +2 -2
  6. data/exe/src +13 -0
  7. data/lib/space_architect/{architect_mission.rb → architect_project.rb} +475 -125
  8. data/lib/space_architect/cli/architect.rb +248 -178
  9. data/lib/space_architect/cli/research.rb +94 -0
  10. data/lib/space_architect/cli/space.rb +25 -31
  11. data/lib/space_architect/cli/src.rb +20 -14
  12. data/lib/space_architect/cli.rb +22 -22
  13. data/lib/space_architect/dispatcher.rb +5 -1
  14. data/lib/space_architect/gate_evaluator.rb +65 -0
  15. data/lib/space_architect/gate_lint.rb +140 -0
  16. data/lib/space_architect/harness.rb +145 -17
  17. data/lib/space_architect/research/mux.rb +127 -0
  18. data/lib/space_architect/research/registry.rb +70 -0
  19. data/lib/space_architect/research/renderer.rb +101 -0
  20. data/lib/space_architect/research/run.rb +7 -0
  21. data/lib/space_architect/research/supervisor.rb +108 -0
  22. data/lib/space_architect/research.rb +13 -0
  23. data/lib/space_architect/run_creator.rb +53 -0
  24. data/lib/space_architect/skill_installer.rb +81 -79
  25. data/lib/space_architect/templates/architect.md.erb +15 -4
  26. data/lib/space_architect/templates/brief.md.erb +5 -5
  27. data/lib/space_architect/templates/iteration.md.erb +17 -6
  28. data/lib/space_architect.rb +8 -21
  29. data/lib/{space_architect → space_core}/atomic_write.rb +1 -1
  30. data/lib/space_core/cli/base_command.rb +19 -0
  31. data/lib/space_core/cli/build.rb +27 -0
  32. data/lib/space_core/cli/config.rb +49 -0
  33. data/lib/space_core/cli/current.rb +16 -0
  34. data/lib/space_core/cli/help.rb +123 -0
  35. data/lib/space_core/cli/helpers.rb +115 -0
  36. data/lib/space_core/cli/init.rb +29 -0
  37. data/lib/space_core/cli/list.rb +24 -0
  38. data/lib/space_core/cli/new.rb +38 -0
  39. data/lib/space_core/cli/pack.rb +29 -0
  40. data/lib/space_core/cli/path.rb +16 -0
  41. data/lib/space_core/cli/repeatable_options.rb +75 -0
  42. data/lib/space_core/cli/repo.rb +76 -0
  43. data/lib/space_core/cli/run.rb +29 -0
  44. data/lib/space_core/cli/shell.rb +125 -0
  45. data/lib/space_core/cli/show.rb +21 -0
  46. data/lib/space_core/cli/status.rb +33 -0
  47. data/lib/space_core/cli/use.rb +17 -0
  48. data/lib/space_core/cli.rb +177 -0
  49. data/lib/{space_architect → space_core}/config.rb +1 -1
  50. data/lib/{space_architect → space_core}/errors.rb +1 -1
  51. data/lib/{space_architect → space_core}/git_client.rb +1 -1
  52. data/lib/{space_architect → space_core}/mise_client.rb +1 -1
  53. data/lib/space_core/oci_builder.rb +56 -0
  54. data/lib/space_core/oci_packer.rb +99 -0
  55. data/lib/space_core/oci_runner.rb +73 -0
  56. data/lib/{space_architect → space_core}/repo_reference.rb +1 -1
  57. data/lib/{space_architect → space_core}/repo_resolver.rb +1 -1
  58. data/lib/{space_architect → space_core}/shell_integration.rb +1 -1
  59. data/lib/{space_architect → space_core}/slugger.rb +1 -1
  60. data/lib/{space_architect → space_core}/space.rb +11 -3
  61. data/lib/{space_architect → space_core}/space_store.rb +13 -13
  62. data/lib/{space_architect → space_core}/state.rb +1 -1
  63. data/lib/space_core/templates/oci/dockerfile.erb +63 -0
  64. data/lib/space_core/templates/oci/dockerignore.erb +17 -0
  65. data/lib/space_core/templates/oci/entrypoint.sh.erb +10 -0
  66. data/lib/{space_architect → space_core}/terminal.rb +1 -1
  67. data/lib/space_core/version.rb +7 -0
  68. data/lib/{space_architect → space_core}/warnings.rb +1 -1
  69. data/lib/{space_architect → space_core}/xdg.rb +1 -1
  70. data/lib/space_core.rb +24 -0
  71. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/cli/clone.rb +5 -5
  72. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/cli/config.rb +7 -7
  73. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/cli/daemon.rb +46 -30
  74. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/cli/options.rb +1 -1
  75. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/cli/org.rb +9 -9
  76. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/cli/repo.rb +9 -9
  77. data/lib/space_src/cli/shell.rb +122 -0
  78. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/cli/status.rb +7 -7
  79. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/cli/sync.rb +17 -17
  80. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/cli.rb +42 -11
  81. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/cloner.rb +3 -3
  82. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/config/contract.rb +1 -1
  83. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/config/duration.rb +1 -1
  84. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/config/model.rb +1 -1
  85. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/config/store.rb +5 -5
  86. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/forge/client.rb +2 -2
  87. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/forge/github.rb +4 -4
  88. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/launchd/agent.rb +5 -5
  89. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/launchd/plist.rb +3 -3
  90. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/log_rotator.rb +1 -1
  91. data/lib/space_src/migration.rb +43 -0
  92. data/lib/space_src/nav.rb +98 -0
  93. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/paths.rb +2 -2
  94. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/scm/client.rb +1 -1
  95. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/scm/git.rb +4 -4
  96. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/scm/status.rb +1 -1
  97. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/shell.rb +1 -1
  98. data/lib/space_src/shell_integration.rb +321 -0
  99. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/state/lock.rb +1 -1
  100. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/state/store.rb +2 -2
  101. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/sync/engine.rb +12 -12
  102. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/sync/repo_plan.rb +3 -3
  103. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/ui/interactive_reporter.rb +1 -1
  104. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/ui/json_reporter.rb +1 -1
  105. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/ui/mode.rb +1 -1
  106. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/ui/plain_reporter.rb +1 -1
  107. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/ui/reporter.rb +1 -1
  108. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/version.rb +2 -2
  109. data/lib/space_src.rb +37 -0
  110. data/skill/architect/SKILL.md +110 -54
  111. data/skill/architect/dispatch.md +147 -39
  112. data/skill/architect/research.md +46 -37
  113. data/skill/architect-research/SKILL.md +2 -2
  114. data/skill/architect-vocabulary/SKILL.md +24 -21
  115. metadata +127 -68
  116. data/lib/space_architect/cli/config.rb +0 -61
  117. data/lib/space_architect/cli/current.rb +0 -22
  118. data/lib/space_architect/cli/helpers.rb +0 -117
  119. data/lib/space_architect/cli/init.rb +0 -35
  120. data/lib/space_architect/cli/list.rb +0 -30
  121. data/lib/space_architect/cli/new.rb +0 -43
  122. data/lib/space_architect/cli/options.rb +0 -12
  123. data/lib/space_architect/cli/path.rb +0 -22
  124. data/lib/space_architect/cli/repo.rb +0 -88
  125. data/lib/space_architect/cli/shell.rb +0 -137
  126. data/lib/space_architect/cli/show.rb +0 -27
  127. data/lib/space_architect/cli/status.rb +0 -39
  128. data/lib/space_architect/cli/use.rb +0 -23
  129. data/lib/space_architect/version.rb +0 -5
  130. data/vendor/repo-tender/lib/space_architect/pristine.rb +0 -44
@@ -1,32 +1,60 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module SpaceArchitect
3
+ require "json"
4
+
5
+ module Space::Architect
4
6
  module CLI
5
7
  module Architect
6
- class Init < Dry::CLI::Command
7
- include GlobalOptions
8
- include Helpers
9
-
10
- desc "Scaffold architect mission memory in the current space"
8
+ class Init < BaseCommand
9
+ desc "Scaffold (or top up) the architect project: ARCHITECT.md, space.yaml project block, SessionStart hook"
11
10
  argument :space, required: false, desc: "Space identifier (default: $PWD)"
12
11
 
13
12
  def call(space: nil, **opts)
14
13
  setup_terminal(**opts.slice(:color, :colors))
15
14
  handle_errors do
16
15
  render(store.find(space)) do |sp|
17
- mission = ArchitectMission.new(space: sp)
18
- path = mission.init!
19
- terminal.say "Mission ready: #{terminal.path(path)}"
16
+ project = ArchitectProject.new(space: sp)
17
+ path = project.init!
18
+ terminal.say "Project ready: #{terminal.path(path)}"
20
19
  CLI.record_outcome(Outcome.new(exit_code: 0))
21
20
  end
22
21
  end
23
22
  end
24
23
  end
25
24
 
26
- class New < Dry::CLI::Command
27
- include GlobalOptions
28
- include Helpers
25
+ class Ground < BaseCommand
26
+ desc "Print grounding reads (ARCHITECT.md, BRIEF.md, in-flight iteration) to stdout"
27
+ argument :space, required: false, desc: "Space identifier (default: $PWD)"
28
+
29
+ def call(space: nil, **opts)
30
+ setup_terminal(**opts.slice(:color, :colors))
31
+ handle_errors do
32
+ session_cwd = parse_session_cwd_from_stdin
33
+ render(store.find(space)) do |sp|
34
+ project = ArchitectProject.new(space: sp)
35
+ content = project.ground(session_cwd: session_cwd)
36
+ terminal.say content unless content.empty?
37
+ CLI.record_outcome(Outcome.new(exit_code: 0))
38
+ end
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ # Read the session's working directory from the Claude Code hook JSON on stdin,
45
+ # falling back to Dir.pwd when stdin is a tty or returns nothing (e.g. direct
46
+ # terminal invocation, CI with /dev/null stdin, or in-process test invocation).
47
+ def parse_session_cwd_from_stdin
48
+ return Dir.pwd if $stdin.tty?
49
+ line = $stdin.gets
50
+ return Dir.pwd unless line
51
+ JSON.parse(line.strip)["cwd"] || Dir.pwd
52
+ rescue JSON::ParserError, TypeError
53
+ Dir.pwd
54
+ end
55
+ end
29
56
 
57
+ class New < BaseCommand
30
58
  desc "Scaffold the next iteration file (architecture/I<NN>-<iteration>.md)"
31
59
  argument :iteration, required: true, desc: "Iteration name (kebab-case)"
32
60
  argument :space, required: false, desc: "Space identifier (default: $PWD)"
@@ -35,8 +63,8 @@ module SpaceArchitect
35
63
  setup_terminal(**opts.slice(:color, :colors))
36
64
  handle_errors do
37
65
  render(store.find(space)) do |sp|
38
- mission = ArchitectMission.new(space: sp)
39
- path = mission.new_iteration!(iteration)
66
+ project = ArchitectProject.new(space: sp)
67
+ path = project.new_iteration!(iteration)
40
68
  terminal.say "Iteration scaffolded: #{terminal.path(path)}"
41
69
  CLI.record_outcome(Outcome.new(exit_code: 0))
42
70
  end
@@ -44,22 +72,19 @@ module SpaceArchitect
44
72
  end
45
73
  end
46
74
 
47
- class Status < Dry::CLI::Command
48
- include GlobalOptions
49
- include Helpers
50
-
51
- desc "Show architect mission state (read-only)"
75
+ class Status < BaseCommand
76
+ desc "Show architect project state (read-only)"
52
77
  argument :space, required: false, desc: "Space identifier (default: $PWD)"
53
78
 
54
79
  def call(space: nil, **opts)
55
80
  setup_terminal(**opts.slice(:color, :colors))
56
81
  handle_errors do
57
82
  render(store.find(space)) do |sp|
58
- mission = ArchitectMission.new(space: sp)
59
- info = mission.status
83
+ project = ArchitectProject.new(space: sp)
84
+ info = project.status
60
85
  block = info[:block]
61
86
 
62
- terminal.say "Mission status: #{block['status'] || '(none)'}"
87
+ terminal.say "Project status: #{block['status'] || '(none)'}"
63
88
  terminal.say "Current iteration: #{block['current_iteration'] || '(none)'}"
64
89
 
65
90
  iterations = block["iterations"] || []
@@ -77,7 +102,14 @@ module SpaceArchitect
77
102
  end.join(", ")
78
103
  lanes = lane_list.any? { |l| l["variant"] } ? "variant: #{lanes_str}" : lanes_str
79
104
  lanes = "#{lanes} → winner: #{s['winner']}" if s["winner"]
80
- [nn, s["name"], s["freeze_sha"]&.[](0, 8) || "-", lanes, s["verdict"] || "-"]
105
+ verdict_str = if s["verdict"] && s["verdict"] != "pending"
106
+ s["verdict"]
107
+ elsif (s["lanes"] || []).any? { |l| l["integration_branch"] }
108
+ "awaiting-verdict"
109
+ else
110
+ s["verdict"] || "-"
111
+ end
112
+ [nn, s["name"], s["freeze_sha"]&.[](0, 8) || "-", lanes, verdict_str]
81
113
  end
82
114
  terminal.say terminal.table(%w[II Iteration FreezeSHA Lanes Verdict], rows)
83
115
  end
@@ -92,11 +124,8 @@ module SpaceArchitect
92
124
  end
93
125
  end
94
126
 
95
- class Freeze < Dry::CLI::Command
96
- include GlobalOptions
97
- include Helpers
98
-
99
- desc "Freeze gates for an iteration"
127
+ class Freeze < BaseCommand
128
+ desc "Freeze the iteration's frozen region (Grounds/Specification/Acceptance Criteria) and record the freeze SHA"
100
129
  argument :iteration, required: true, desc: "Iteration name"
101
130
  argument :space, required: false, desc: "Space identifier (default: $PWD)"
102
131
 
@@ -104,10 +133,12 @@ module SpaceArchitect
104
133
  setup_terminal(**opts.slice(:color, :colors))
105
134
  handle_errors do
106
135
  render(store.find(space)) do |sp|
107
- mission = ArchitectMission.new(space: sp)
108
- sha = mission.freeze!(iteration)
136
+ project = ArchitectProject.new(space: sp)
137
+ warnings = []
138
+ sha = project.freeze!(iteration, warnings: warnings)
109
139
  terminal.say "Frozen #{iteration} at #{sha}"
110
- ac = mission.acceptance_criteria(iteration)
140
+ warnings.each { |w| terminal.say "Warning: #{w}" }
141
+ ac = project.acceptance_criteria(iteration)
111
142
  unless ac.to_s.strip.empty?
112
143
  terminal.say ""
113
144
  terminal.say "Frozen Acceptance Criteria (quote these verbatim when judging):"
@@ -119,11 +150,8 @@ module SpaceArchitect
119
150
  end
120
151
  end
121
152
 
122
- class Verify < Dry::CLI::Command
123
- include GlobalOptions
124
- include Helpers
125
-
126
- desc "Post-flight mechanical checks for an iteration (reports only, no judgment)"
153
+ class Verify < BaseCommand
154
+ desc "Post-flight mechanical lane checks — frozen-untouched, no builder commits, report exists, in-bounds (reports only, no judgment)"
127
155
  argument :iteration, required: true, desc: "Iteration name"
128
156
  argument :space, required: false, desc: "Space identifier (default: $PWD)"
129
157
 
@@ -131,8 +159,8 @@ module SpaceArchitect
131
159
  setup_terminal(**opts.slice(:color, :colors))
132
160
  handle_errors do
133
161
  render(store.find(space)) do |sp|
134
- mission = ArchitectMission.new(space: sp)
135
- results = mission.verify(iteration)
162
+ project = ArchitectProject.new(space: sp)
163
+ results = project.verify(iteration)
136
164
 
137
165
  if results.empty?
138
166
  terminal.say "No lanes recorded for iteration '#{iteration}'"
@@ -161,50 +189,69 @@ module SpaceArchitect
161
189
 
162
190
  def pass_fail(val)
163
191
  case val
164
- when true then "PASS"
165
- when false then "FAIL"
166
- else "N/A"
192
+ when true then "PASS"
193
+ when false then "FAIL"
194
+ when :no_touch_set then "WARN — no touch_set recorded"
195
+ else "N/A"
167
196
  end
168
197
  end
169
198
  end
170
199
 
171
- class Dispatch < Dry::CLI::Command
172
- include GlobalOptions
173
- include Helpers
174
-
200
+ class Dispatch < BaseCommand
175
201
  desc "Dispatch a builder for a lane (streams to build/<id>-<lane>/run.jsonl)"
176
202
  argument :iteration, required: true, desc: "Iteration name"
177
203
  argument :lane, required: true, desc: "Lane name"
178
204
  argument :space, required: false, desc: "Space identifier (default: $PWD)"
179
- option :model, default: nil, desc: "Model to use (default: lane entry or claude-sonnet-4-6)"
180
- option :max_turns, default: "200", desc: "Max turns for the builder"
181
- option :harness, default: nil, desc: "Harness override (claude-code, opencode)"
182
- option :effort, default: nil, desc: "Reasoning effort override (opencode only; sets reasoningEffort in the model config)"
205
+ 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"
206
+ option :max_turns, default: "200", desc: "Max turns for the builder"
207
+ option :harness, default: nil, desc: "Harness override (claude-code, opencode)"
208
+ option :effort, default: nil, desc: "Reasoning effort override (opencode only; sets reasoningEffort in the model config)"
209
+ option :detach, type: :boolean, default: false, desc: "Detach the builder process (returns immediately with PID; poll report for completion)"
210
+ option :timeout, default: "14400", desc: "Wall-clock timeout in seconds (0 disables; default 4h); foreground only"
211
+ option :push_url, default: nil, desc: "HTTP endpoint for streaming push (POST body to this URL)"
212
+ option :push_token, default: nil, desc: "Bearer token for push endpoint authorization"
213
+ 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)"
183
214
 
184
215
  def call(iteration:, lane:, space: nil, model: nil,
185
- max_turns: "200", harness: nil, effort: nil, **opts)
216
+ max_turns: "200", harness: nil, effort: nil, detach: false,
217
+ timeout: "14400", push_url: nil, push_token: nil, push_host: nil, **opts)
186
218
  setup_terminal(**opts.slice(:color, :colors))
187
219
  handle_errors do
188
220
  render(store.find(space)) do |sp|
189
- mission = ArchitectMission.new(space: sp)
190
- kwargs = { max_turns: max_turns.to_i }
191
- kwargs[:model] = model if model
192
- kwargs[:harness] = harness if harness
193
- kwargs[:effort] = effort if effort
194
- res = mission.dispatch(iteration, lane, **kwargs)
195
- terminal.say "Run log: #{terminal.path(res[:run_log])}"
196
- terminal.say "Report: #{terminal.path(res[:report])}"
197
- terminal.say "Builder exited with status #{res[:exit_code]}"
198
- CLI.record_outcome(Outcome.new(exit_code: res[:exit_code]))
221
+ project = ArchitectProject.new(space: sp)
222
+ kwargs = { max_turns: max_turns.to_i, detach: detach }
223
+ kwargs[:model] = model if model
224
+ kwargs[:harness] = harness if harness
225
+ kwargs[:effort] = effort if effort
226
+ kwargs[:timeout] = timeout.to_i unless detach
227
+ kwargs[:push_url] = push_url if push_url
228
+ kwargs[:push_token] = push_token if push_token
229
+ kwargs[:push_host] = push_host if push_host
230
+ res = project.dispatch(iteration, lane, **kwargs)
231
+ if detach
232
+ terminal.say "PID: #{res[:pid]}"
233
+ terminal.say "Run log: #{terminal.path(res[:run_log])}"
234
+ terminal.say "Report: #{terminal.path(res[:report])}"
235
+ terminal.say "Dispatched detached — poll #{terminal.path(res[:report])} for completion"
236
+ CLI.record_outcome(Outcome.new(exit_code: 0))
237
+ elsif res[:timed_out]
238
+ terminal.say "Run log: #{terminal.path(res[:run_log])}"
239
+ terminal.say "Report: #{terminal.path(res[:report])}"
240
+ terminal.say "Builder TIMED OUT after #{timeout}s — process group killed. Re-dispatch (lanes are cheap)."
241
+ CLI.record_outcome(Outcome.new(exit_code: res[:exit_code]))
242
+ else
243
+ terminal.say "Run log: #{terminal.path(res[:run_log])}"
244
+ terminal.say "Report: #{terminal.path(res[:report])}"
245
+ terminal.say "Ingest URL: #{res[:push_url]}" if res[:push_url]
246
+ terminal.say "Builder exited with status #{res[:exit_code]}"
247
+ CLI.record_outcome(Outcome.new(exit_code: res[:exit_code]))
248
+ end
199
249
  end
200
250
  end
201
251
  end
202
252
  end
203
253
 
204
- class Section < Dry::CLI::Command
205
- include GlobalOptions
206
- include Helpers
207
-
254
+ class Section < BaseCommand
208
255
  desc "Write a section of the iteration file and commit it (one call)"
209
256
  argument :iteration, required: true, desc: "Iteration name"
210
257
  argument :section, required: true, desc: "Section: grounds, specification, prompt, verdict"
@@ -220,8 +267,8 @@ module SpaceArchitect
220
267
  handle_errors do
221
268
  content = read_section_body(from: from, body: body, stdin: stdin)
222
269
  render(store.find(space)) do |sp|
223
- mission = ArchitectMission.new(space: sp)
224
- res = mission.write_section!(iteration, section, body: content, append: append, lane: lane)
270
+ project = ArchitectProject.new(space: sp)
271
+ res = project.write_section!(iteration, section, body: content, append: append, lane: lane)
225
272
  if res[:committed]
226
273
  terminal.say "Committed #{res[:heading]} → #{res[:sha][0, 8]}"
227
274
  terminal.say res[:diffstat] unless res[:diffstat].empty?
@@ -239,14 +286,43 @@ module SpaceArchitect
239
286
  return File.read(from) if from
240
287
  return body if body
241
288
  return $stdin.read if stdin
242
- raise SpaceArchitect::Error, "provide the section body via --from <file>, --body <text>, or --stdin"
289
+ raise Space::Core::Error, "provide the section body via --from <file>, --body <text>, or --stdin"
243
290
  end
244
291
  end
245
292
 
246
- class Evidence < Dry::CLI::Command
247
- include GlobalOptions
248
- include Helpers
293
+ class Verdict < BaseCommand
294
+ desc "Record the architect's verdict decision (continue or kill) and write ## Verdict prose"
295
+ argument :iteration, required: true, desc: "Iteration name"
296
+ argument :decision, required: true, desc: "Decision: continue or kill"
297
+ argument :space, required: false, desc: "Space identifier (default: $PWD)"
298
+ option :from, default: nil, desc: "Read the verdict body from this file"
299
+ option :body, default: nil, desc: "Inline verdict body"
300
+ option :stdin, type: :boolean, default: false, desc: "Read the verdict body from stdin"
249
301
 
302
+ def call(iteration:, decision:, space: nil, from: nil, body: nil, stdin: false, **opts)
303
+ setup_terminal(**opts.slice(:color, :colors))
304
+ handle_errors do
305
+ content = read_section_body(from: from, body: body, stdin: stdin)
306
+ render(store.find(space)) do |sp|
307
+ project = ArchitectProject.new(space: sp)
308
+ res = project.record_verdict!(iteration, decision: decision, body: content)
309
+ terminal.say "Verdict '#{res[:decision]}' recorded → #{res[:sha][0, 8]}"
310
+ CLI.record_outcome(Outcome.new(exit_code: 0))
311
+ end
312
+ end
313
+ end
314
+
315
+ private
316
+
317
+ def read_section_body(from:, body:, stdin:)
318
+ return File.read(from) if from
319
+ return body if body
320
+ return $stdin.read if stdin
321
+ raise Space::Core::Error, "provide the verdict body via --from <file>, --body <text>, or --stdin"
322
+ end
323
+ end
324
+
325
+ class Evidence < BaseCommand
250
326
  desc "Transcribe a lane's scratch report VERBATIM into Builder Report and commit"
251
327
  argument :iteration, required: true, desc: "Iteration name"
252
328
  argument :space, required: false, desc: "Space identifier (default: $PWD)"
@@ -256,8 +332,8 @@ module SpaceArchitect
256
332
  setup_terminal(**opts.slice(:color, :colors))
257
333
  handle_errors do
258
334
  render(store.find(space)) do |sp|
259
- mission = ArchitectMission.new(space: sp)
260
- res = mission.transcribe_evidence!(iteration, lane: lane)
335
+ project = ArchitectProject.new(space: sp)
336
+ res = project.transcribe_evidence!(iteration, lane: lane)
261
337
  terminal.say "Transcribed #{res[:lines]} lines → #{res[:sha][0, 8]}"
262
338
  terminal.say "Builder STATUS: #{res[:status_line]}" if res[:status_line]
263
339
  terminal.say "Now rule on the builder's PHASE 0 disagreements in the Verdict (a later session)."
@@ -267,10 +343,7 @@ module SpaceArchitect
267
343
  end
268
344
  end
269
345
 
270
- class Merge < Dry::CLI::Command
271
- include GlobalOptions
272
- include Helpers
273
-
346
+ class Merge < BaseCommand
274
347
  desc "Integrate ONE judged-passing lane (merges --no-ff; runs no gates, makes no verdict)"
275
348
  argument :iteration, required: true, desc: "Iteration name"
276
349
  argument :lane, required: true, desc: "Lane name (architect-judged passing)"
@@ -281,8 +354,8 @@ module SpaceArchitect
281
354
  setup_terminal(**opts.slice(:color, :colors))
282
355
  handle_errors do
283
356
  render(store.find(space)) do |sp|
284
- mission = ArchitectMission.new(space: sp)
285
- r = mission.merge_lane!(iteration, lane, message: message)
357
+ project = ArchitectProject.new(space: sp)
358
+ r = project.merge_lane!(iteration, lane, message: message)
286
359
  terminal.say "Merged #{lane} → #{r[:integration_branch]} (#{r[:merge_sha][0, 8]})"
287
360
  terminal.say r[:diffstat] unless r[:diffstat].empty?
288
361
  terminal.say "Gates NOT run — run `architect gate #{iteration}` against the integration branch."
@@ -292,10 +365,7 @@ module SpaceArchitect
292
365
  end
293
366
  end
294
367
 
295
- class Integrate < Dry::CLI::Command
296
- include GlobalOptions
297
- include Helpers
298
-
368
+ class Integrate < BaseCommand
299
369
  desc "Integrate the architect-supplied set of passing lanes, in order (stops on conflict)"
300
370
  argument :iteration, required: true, desc: "Iteration name"
301
371
  argument :space, required: false, desc: "Space identifier (default: $PWD)"
@@ -306,9 +376,9 @@ module SpaceArchitect
306
376
  setup_terminal(**opts.slice(:color, :colors))
307
377
  handle_errors do
308
378
  render(store.find(space)) do |sp|
309
- mission = ArchitectMission.new(space: sp)
379
+ project = ArchitectProject.new(space: sp)
310
380
  lane_names = lanes.to_s.split(",").map(&:strip).reject(&:empty?)
311
- results = mission.integrate!(iteration, lanes: lane_names, teardown: teardown)
381
+ results = project.integrate!(iteration, lanes: lane_names, teardown: teardown)
312
382
  results.each do |r|
313
383
  terminal.say "Merged #{r[:lane]} → #{r[:integration_branch]} (#{r[:merge_sha][0, 8]})"
314
384
  end
@@ -319,11 +389,29 @@ module SpaceArchitect
319
389
  end
320
390
  end
321
391
 
322
- class Gate < Dry::CLI::Command
323
- include GlobalOptions
324
- include Helpers
392
+ class Land < BaseCommand
393
+ desc "Generate the end-of-project PR command (no push, no gh — prints gh pr create)"
394
+ argument :space, required: false, desc: "Space identifier (default: $PWD)"
325
395
 
326
- desc "Run the frozen Acceptance Criteria gate commands and stream raw output (no PASS/FAIL)"
396
+ def call(space: nil, **opts)
397
+ setup_terminal(**opts.slice(:color, :colors))
398
+ handle_errors do
399
+ render(store.find(space)) do |sp|
400
+ project = ArchitectProject.new(space: sp)
401
+ results = project.land
402
+ results.each do |r|
403
+ terminal.say r[:context]
404
+ terminal.say r[:command]
405
+ terminal.say "Body: #{terminal.path(r[:body_file])}"
406
+ end
407
+ CLI.record_outcome(Outcome.new(exit_code: 0))
408
+ end
409
+ end
410
+ end
411
+ end
412
+
413
+ class Gate < BaseCommand
414
+ desc "Run the frozen Acceptance Criteria gate commands and report PASS/FAIL"
327
415
  argument :iteration, required: true, desc: "Iteration name"
328
416
  argument :lane, required: false, desc: "Run in a lane worktree (default: the integration repo)"
329
417
  argument :space, required: false, desc: "Space identifier (default: $PWD)"
@@ -332,26 +420,26 @@ module SpaceArchitect
332
420
  setup_terminal(**opts.slice(:color, :colors))
333
421
  handle_errors do
334
422
  render(store.find(space)) do |sp|
335
- mission = ArchitectMission.new(space: sp)
336
- results = mission.run_gates(iteration, lane: lane)
423
+ project = ArchitectProject.new(space: sp)
424
+ results = project.run_gates(iteration, lane: lane)
337
425
  results.each do |r|
426
+ marker = r[:status] == :pass ? "PASS" : "FAIL"
338
427
  terminal.say ""
339
- terminal.say "── #{r[:ac].empty? ? "(gate)" : r[:ac]}: #{r[:command]} (exit #{r[:exit_code]})"
428
+ terminal.say "── #{r[:ac].empty? ? "(gate)" : r[:ac]}: #{r[:cmd]} (exit #{r[:exit_code]}) [#{marker}]"
429
+ terminal.say " reason: #{r[:reason]}" if r[:status] == :fail && !r[:reason].to_s.empty?
340
430
  terminal.say r[:stdout].rstrip unless r[:stdout].strip.empty?
341
431
  terminal.say r[:stderr].rstrip unless r[:stderr].strip.empty?
342
432
  end
343
433
  terminal.say ""
344
- terminal.say "Raw gate output above the PASS/FAIL/INVALID verdict is yours, read against the frozen thresholds."
345
- CLI.record_outcome(Outcome.new(exit_code: 0))
434
+ terminal.say "Mechanical gate results above; the Acceptance-Criteria verdict necessary, not sufficient — remains the architect's."
435
+ any_fail = results.any? { |r| r[:status] == :fail }
436
+ CLI.record_outcome(Outcome.new(exit_code: any_fail ? 1 : 0))
346
437
  end
347
438
  end
348
439
  end
349
440
  end
350
441
 
351
- class InstallSkills < Dry::CLI::Command
352
- include GlobalOptions
353
- include Helpers
354
-
442
+ class InstallSkills < BaseCommand
355
443
  desc "Install bundled skills (architect, architect-research, architect-vocabulary) for a harness"
356
444
  option :provider, default: "claude", desc: "Harness: claude, codex, opencode, pi"
357
445
  option :project, type: :boolean, default: false, desc: "Install to CWD instead of global"
@@ -373,34 +461,8 @@ module SpaceArchitect
373
461
  end
374
462
  end
375
463
 
376
- module Brief
377
- class New < Dry::CLI::Command
378
- include GlobalOptions
379
- include Helpers
380
-
381
- desc "Scaffold the durable mission brief (architecture/BRIEF.md)"
382
- argument :space, required: false, desc: "Space identifier (default: $PWD)"
383
- option :force, type: :boolean, default: false, desc: "Overwrite an existing BRIEF.md"
384
-
385
- def call(space: nil, force: false, **opts)
386
- setup_terminal(**opts.slice(:color, :colors))
387
- handle_errors do
388
- render(store.find(space)) do |sp|
389
- mission = ArchitectMission.new(space: sp)
390
- path = mission.brief_new!(force: force)
391
- terminal.say "Brief ready: #{terminal.path(path)}"
392
- CLI.record_outcome(Outcome.new(exit_code: 0))
393
- end
394
- end
395
- end
396
- end
397
- end
398
-
399
464
  module Worktree
400
- class Add < Dry::CLI::Command
401
- include GlobalOptions
402
- include Helpers
403
-
465
+ class Add < BaseCommand
404
466
  desc "Create a worktree for a lane"
405
467
  argument :repo, required: true, desc: "Repo name (under repos/)"
406
468
  argument :iteration, required: true, desc: "Iteration name"
@@ -415,9 +477,9 @@ module SpaceArchitect
415
477
  setup_terminal(**opts.slice(:color, :colors))
416
478
  handle_errors do
417
479
  render(store.find) do |sp|
418
- mission = ArchitectMission.new(space: sp)
480
+ project = ArchitectProject.new(space: sp)
419
481
  touch_set = touch ? touch.split(",").map(&:strip).reject(&:empty?) : nil
420
- result = mission.worktree_add(repo, iteration, lane, base: base,
482
+ result = project.worktree_add(repo, iteration, lane, base: base,
421
483
  harness: harness, model: model, effort: effort, touch: touch_set)
422
484
  terminal.say "Worktree: #{terminal.path(result[:worktree])}"
423
485
  terminal.say "Base SHA: #{result[:base_sha]}"
@@ -427,10 +489,7 @@ module SpaceArchitect
427
489
  end
428
490
  end
429
491
 
430
- class Remove < Dry::CLI::Command
431
- include GlobalOptions
432
- include Helpers
433
-
492
+ class Remove < BaseCommand
434
493
  desc "Remove a lane worktree"
435
494
  argument :iteration, required: true, desc: "Iteration name"
436
495
  argument :lane, required: true, desc: "Lane name"
@@ -439,8 +498,8 @@ module SpaceArchitect
439
498
  setup_terminal(**opts.slice(:color, :colors))
440
499
  handle_errors do
441
500
  render(store.find) do |sp|
442
- mission = ArchitectMission.new(space: sp)
443
- mission.worktree_remove(iteration, lane)
501
+ project = ArchitectProject.new(space: sp)
502
+ project.worktree_remove(iteration, lane)
444
503
  terminal.say "Removed worktree for #{iteration}/#{lane}"
445
504
  CLI.record_outcome(Outcome.new(exit_code: 0))
446
505
  end
@@ -448,18 +507,15 @@ module SpaceArchitect
448
507
  end
449
508
  end
450
509
 
451
- class List < Dry::CLI::Command
452
- include GlobalOptions
453
- include Helpers
454
-
510
+ class List < BaseCommand
455
511
  desc "List active architect worktrees"
456
512
 
457
513
  def call(**opts)
458
514
  setup_terminal(**opts.slice(:color, :colors))
459
515
  handle_errors do
460
516
  render(store.find) do |sp|
461
- mission = ArchitectMission.new(space: sp)
462
- worktrees = mission.worktree_list
517
+ project = ArchitectProject.new(space: sp)
518
+ worktrees = project.worktree_list
463
519
  if worktrees.empty?
464
520
  terminal.say "No active architect worktrees"
465
521
  else
@@ -473,10 +529,7 @@ module SpaceArchitect
473
529
  end
474
530
 
475
531
  module Variant
476
- class Add < Dry::CLI::Command
477
- include GlobalOptions
478
- include Helpers
479
-
532
+ class Add < BaseCommand
480
533
  desc "Create a variant set (competing lanes over one frozen spec)"
481
534
  argument :repo, required: true, desc: "Repo name (under repos/)"
482
535
  argument :iteration, required: true, desc: "Iteration name"
@@ -495,8 +548,8 @@ module SpaceArchitect
495
548
  [harness, model]
496
549
  end
497
550
 
498
- mission = ArchitectMission.new(space: sp)
499
- variants = mission.variant_add(repo, iteration, parsed_pairs, base: base, prompt: prompt)
551
+ project = ArchitectProject.new(space: sp)
552
+ variants = project.variant_add(repo, iteration, parsed_pairs, base: base, prompt: prompt)
500
553
  variants.each do |v|
501
554
  terminal.say "#{v[:name]} · #{v[:harness]} · #{v[:model] || "(default)"} · #{terminal.path(v[:worktree])}"
502
555
  end
@@ -506,10 +559,7 @@ module SpaceArchitect
506
559
  end
507
560
  end
508
561
 
509
- class Promote < Dry::CLI::Command
510
- include GlobalOptions
511
- include Helpers
512
-
562
+ class Promote < BaseCommand
513
563
  desc "Promote one variant of a variant set as the winner"
514
564
  argument :iteration, required: true, desc: "Iteration name"
515
565
  argument :winner, required: true, desc: "Variant lane name to promote (e.g. v02)"
@@ -519,8 +569,8 @@ module SpaceArchitect
519
569
  setup_terminal(**opts.slice(:color, :colors))
520
570
  handle_errors do
521
571
  render(store.find(space)) do |sp|
522
- mission = ArchitectMission.new(space: sp)
523
- result = mission.variant_promote(iteration, winner)
572
+ project = ArchitectProject.new(space: sp)
573
+ result = project.variant_promote(iteration, winner)
524
574
  if result[:discarded].any?
525
575
  terminal.say "Promoted #{result[:winner]} (discarded: #{result[:discarded].join(', ')})"
526
576
  else
@@ -532,10 +582,7 @@ module SpaceArchitect
532
582
  end
533
583
  end
534
584
 
535
- class Compare < Dry::CLI::Command
536
- include GlobalOptions
537
- include Helpers
538
-
585
+ class Compare < BaseCommand
539
586
  desc "Compare variants of an iteration's variant set (read-only)"
540
587
  argument :iteration, required: true, desc: "Iteration name"
541
588
  argument :space, required: false, desc: "Space identifier (default: $PWD)"
@@ -544,8 +591,8 @@ module SpaceArchitect
544
591
  setup_terminal(**opts.slice(:color, :colors))
545
592
  handle_errors do
546
593
  render(store.find(space)) do |sp|
547
- mission = ArchitectMission.new(space: sp)
548
- info = mission.variant_compare(iteration)
594
+ project = ArchitectProject.new(space: sp)
595
+ info = project.variant_compare(iteration)
549
596
 
550
597
  terminal.say "Variant comparison: #{iteration} (freeze #{info[:freeze_sha]&.[](0, 8) || "-"})"
551
598
  terminal.say "Winner: #{info[:winner] || '(none)'}"
@@ -569,32 +616,55 @@ module SpaceArchitect
569
616
  end
570
617
  end
571
618
  end
619
+
620
+ module Brief
621
+ class New < BaseCommand
622
+ desc "Scaffold the durable project brief (architecture/BRIEF.md)"
623
+ argument :space, required: false, desc: "Space identifier (default: $PWD)"
624
+ option :force, type: :boolean, default: false, desc: "Overwrite an existing BRIEF.md"
625
+
626
+ def call(space: nil, force: false, **opts)
627
+ setup_terminal(**opts.slice(:color, :colors))
628
+ handle_errors do
629
+ render(store.find(space)) do |sp|
630
+ project = ArchitectProject.new(space: sp)
631
+ path = project.brief_new!(force: force)
632
+ terminal.say "Brief ready: #{terminal.path(path)}"
633
+ CLI.record_outcome(Outcome.new(exit_code: 0))
634
+ end
635
+ end
636
+ end
637
+ end
638
+ end
572
639
  end
573
640
  end
574
641
  end
575
642
 
576
- SpaceArchitect::CLI::Registry.register "init", SpaceArchitect::CLI::Architect::Init
577
- SpaceArchitect::CLI::Registry.register "new", SpaceArchitect::CLI::Architect::New
578
- SpaceArchitect::CLI::Registry.register "status", SpaceArchitect::CLI::Architect::Status
579
- SpaceArchitect::CLI::Registry.register "freeze", SpaceArchitect::CLI::Architect::Freeze
580
- SpaceArchitect::CLI::Registry.register "verify", SpaceArchitect::CLI::Architect::Verify
581
- SpaceArchitect::CLI::Registry.register "dispatch", SpaceArchitect::CLI::Architect::Dispatch
582
- SpaceArchitect::CLI::Registry.register "section", SpaceArchitect::CLI::Architect::Section
583
- SpaceArchitect::CLI::Registry.register "evidence", SpaceArchitect::CLI::Architect::Evidence
584
- SpaceArchitect::CLI::Registry.register "merge", SpaceArchitect::CLI::Architect::Merge
585
- SpaceArchitect::CLI::Registry.register "integrate", SpaceArchitect::CLI::Architect::Integrate
586
- SpaceArchitect::CLI::Registry.register "gate", SpaceArchitect::CLI::Architect::Gate
587
- SpaceArchitect::CLI::Registry.register "install-skills", SpaceArchitect::CLI::Architect::InstallSkills
588
- SpaceArchitect::CLI::Registry.register "brief" do |b|
589
- b.register "new", SpaceArchitect::CLI::Architect::Brief::New
643
+ Space::Architect::CLI::Registry.register "init", Space::Architect::CLI::Architect::Init
644
+ Space::Architect::CLI::Registry.register "ground", Space::Architect::CLI::Architect::Ground
645
+ Space::Architect::CLI::Registry.register "new", Space::Architect::CLI::Architect::New
646
+ Space::Architect::CLI::Registry.register "status", Space::Architect::CLI::Architect::Status
647
+ Space::Architect::CLI::Registry.register "freeze", Space::Architect::CLI::Architect::Freeze
648
+ Space::Architect::CLI::Registry.register "verify", Space::Architect::CLI::Architect::Verify
649
+ Space::Architect::CLI::Registry.register "dispatch", Space::Architect::CLI::Architect::Dispatch
650
+ Space::Architect::CLI::Registry.register "section", Space::Architect::CLI::Architect::Section
651
+ Space::Architect::CLI::Registry.register "verdict", Space::Architect::CLI::Architect::Verdict
652
+ Space::Architect::CLI::Registry.register "evidence", Space::Architect::CLI::Architect::Evidence
653
+ Space::Architect::CLI::Registry.register "merge", Space::Architect::CLI::Architect::Merge
654
+ Space::Architect::CLI::Registry.register "integrate", Space::Architect::CLI::Architect::Integrate
655
+ Space::Architect::CLI::Registry.register "land", Space::Architect::CLI::Architect::Land
656
+ Space::Architect::CLI::Registry.register "gate", Space::Architect::CLI::Architect::Gate
657
+ Space::Architect::CLI::Registry.register "install-skills", Space::Architect::CLI::Architect::InstallSkills
658
+ Space::Architect::CLI::Registry.register "brief" do |b|
659
+ b.register "new", Space::Architect::CLI::Architect::Brief::New
590
660
  end
591
- SpaceArchitect::CLI::Registry.register "worktree" do |wt|
592
- wt.register "add", SpaceArchitect::CLI::Architect::Worktree::Add
593
- wt.register "remove", SpaceArchitect::CLI::Architect::Worktree::Remove
594
- wt.register "list", SpaceArchitect::CLI::Architect::Worktree::List
661
+ Space::Architect::CLI::Registry.register "worktree" do |wt|
662
+ wt.register "add", Space::Architect::CLI::Architect::Worktree::Add
663
+ wt.register "remove", Space::Architect::CLI::Architect::Worktree::Remove
664
+ wt.register "list", Space::Architect::CLI::Architect::Worktree::List
595
665
  end
596
- SpaceArchitect::CLI::Registry.register "variant" do |v|
597
- v.register "add", SpaceArchitect::CLI::Architect::Variant::Add
598
- v.register "promote", SpaceArchitect::CLI::Architect::Variant::Promote
599
- v.register "compare", SpaceArchitect::CLI::Architect::Variant::Compare
666
+ Space::Architect::CLI::Registry.register "variant" do |v|
667
+ v.register "add", Space::Architect::CLI::Architect::Variant::Add
668
+ v.register "promote", Space::Architect::CLI::Architect::Variant::Promote
669
+ v.register "compare", Space::Architect::CLI::Architect::Variant::Compare
600
670
  end