space-architect 2.0.1 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4ded62e199397e162bba1ff6ee37a5c6a9c7ee83a446d154107036aa4d628768
4
- data.tar.gz: 592a6bb0c3b10b2773873e7fc3fd79cf7197bad78ada5520c5c9dc77e60aae2f
3
+ metadata.gz: 135a6282cfe3e3073265b71867e7d228673cbfb9b7253feaaba5ad67a180b7cb
4
+ data.tar.gz: 0d0406a99feadadebb24a6167c11837de25bede933e752ef6507524208f4e5a7
5
5
  SHA512:
6
- metadata.gz: 91b5a865cb40557085399853e723e7763c2a54cb183d95b9744f9886753579aa841309f3f2dc490d28a3ad10d4693db06cfb615a9716b2b13fe68ded86b932f6
7
- data.tar.gz: 6ebb0c24e6e603252aab77da49ff2b904d22084e0c840f7d46d5ac0eb66ec7c315ec04816d6446dc537cac5b3dbc58c7663d8bbbb563443566cd6e84de5ee05a
6
+ metadata.gz: c8b161eff4f4b5a788c2e3c9b423bcc31a495915a076afb70445f6b0cdaf1cc2573ee381ce0c3e09ca712a8398b70ca091b015260b6b4e9c24914ece4a0f5539
7
+ data.tar.gz: f61cdd897881530ad2e81df5bd846c4e644fb85ac6847c4afd147e643b6045b59c7ecb89fd12b83f9a7b5c2ef12813c5e88e7634449a919aac2e4bc75052a6b0
data/CHANGELOG.md CHANGED
@@ -5,6 +5,49 @@ 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
+ ## [3.0.0] - 2026-07-01
9
+
10
+ ### Added
11
+
12
+ - **`architect bug-report`** — zero-friction bug-filing command. Gathers
13
+ diagnostics (gem version, Ruby version/platform, and when run inside a space:
14
+ space id, title, and iteration list with verdicts), writes a prefilled GitHub
15
+ issue-body template to `<space>/build/bug-report/architect-bug-report-YYYYMMDD-HHMMSS.md`
16
+ (or `./architect-bug-report-YYYYMMDD-HHMMSS.md` outside a space — timestamped so
17
+ back-to-back runs in the same session never overwrite each other), and prints —
18
+ never executes — the `gh issue create -R jetpks/space-architect` invocation to run.
19
+
20
+ ### Removed
21
+
22
+ - **BREAKING — `architect land` removed** (added in 2.0.0) — landing is the architect
23
+ skill's procedure: the architect writes the PR body and presents the push +
24
+ PR command. The command as shipped never produced a runnable block (#25) and
25
+ authored content the CLI has no context for (#24).
26
+
27
+ ### Fixed
28
+
29
+ - **`architect bug-report` — `~`-contracted paths in printed commands.**
30
+ The `gh issue create` invocation printed to stdout renders the
31
+ `--body-file` path as `~/…` instead of the user's expanded `$HOME`.
32
+ `Space::Core::Paths.contract` is the single home-contraction helper; `Terminal#path`
33
+ delegates to it.
34
+ - **Command wrapping for narrow terminals.** The `gh issue create` command is
35
+ now rendered with trailing ` \` continuations broken at `--flag` boundaries,
36
+ continuation lines indented two spaces. The wrapped output is valid shell
37
+ (`bash -n` clean). `Space::Core::Commands.wrap` is the single wrapping helper.
38
+
39
+ ## [2.0.2] - 2026-07-01
40
+
41
+ ### Added
42
+
43
+ - **Declarative runtime env forwarding for `space run`.** A space can declare
44
+ `run.env:` in `space.yaml` — a list of host env vars forwarded into the
45
+ container as bare `-e VAR` passthrough (value never lands in argv/`ps`), on top
46
+ of the always-on substrate auth vars. `space run --env VAR` (repeatable) adds
47
+ more ad hoc. A requested-but-unset var warns on stderr instead of failing
48
+ opaquely in-guest. This lets credentialed payloads (e.g. a Fireworks/OpenAI key)
49
+ run via a plain `space run <cmd>` with no wall of flags, without baking secrets.
50
+
8
51
  ## [2.0.1] - 2026-07-01
9
52
 
10
53
  Fast-follow completing the `pack.persist:` story from 2.0.0.
@@ -316,6 +359,7 @@ the **BREAKING** items under Changed. The repository is now a monorepo; the
316
359
  lanes, worktrees, and headless `claude -p` dispatch, with variant sets and the
317
360
  claude-code and opencode harnesses.
318
361
 
362
+ [2.0.2]: https://github.com/jetpks/space-architect/compare/v2.0.1...v2.0.2
319
363
  [2.0.1]: https://github.com/jetpks/space-architect/compare/v2.0.0...v2.0.1
320
364
  [2.0.0]: https://github.com/jetpks/space-architect/compare/v1.3.0...v2.0.0
321
365
  [1.3.0]: https://github.com/jetpks/space-architect/compare/v1.2.0...v1.3.0
data/README.md CHANGED
@@ -158,6 +158,19 @@ variables that are actually set — `ANTHROPIC_API_KEY`, `CLAUDE_CODE_OAUTH_TOKE
158
158
  `ANTHROPIC_BASE_URL` — with `-e` at run time, so credentials live in your shell,
159
159
  never in the image. 🗝️
160
160
 
161
+ **Forwarding payload credentials.** Beyond the always-on auth trio, a space can declare
162
+ `run.env:` in `space.yaml` — a list of host env var names forwarded into the container at
163
+ run time. You can also pass `space run --env VAR` (repeatable) for ad hoc additions. All
164
+ forwarding is bare `-e VAR` passthrough: values never appear in argv, `ps`, or the image.
165
+ A requested-but-unset var warns on stderr instead of silently failing inside the guest.
166
+
167
+ ```yaml
168
+ run:
169
+ env: # run-time: host var names, forwarded as bare -e VAR
170
+ - FIREWORKS_API_KEY
171
+ - OPENAI_API_KEY
172
+ ```
173
+
161
174
  **Declaring provisioning & persistence** — two optional keys in `space.yaml`:
162
175
 
163
176
  ```yaml
@@ -169,10 +182,19 @@ pack:
169
182
  - /root/.local/state
170
183
  ```
171
184
 
172
- `provision` scripts must live under the space root and run as `RUN /space/<script>`
173
- while the image builds. `persist` paths must be absolute; `space run` bind-mounts
174
- each one from `<space>/.state<path>` on the host (created on first run) so a
175
- container's mutable state survives across runs. Both are validated at pack time.
185
+ `provision` scripts must live under the space root and be executable. Each script is
186
+ copied into the image individually (`COPY <script> /space/<script>`) and then invoked
187
+ (`RUN /space/<script>`) **before** the full space tree lands and **before** the gem is
188
+ installed. This ordering is the cache-hygiene guarantee: editing any other space file
189
+ leaves the provision and gem-install layers cached, so a rebuild completes in seconds
190
+ instead of minutes. Scripts must therefore be self-contained — they run with only the
191
+ base system layers and any earlier provision scripts' outputs; they cannot read other
192
+ space files or call `architect`/`space`. Scripts must have the executable bit set; COPY
193
+ preserves the mode from the build context.
194
+
195
+ `persist` paths must be absolute; `space run` bind-mounts each one from
196
+ `<space>/.state<path>` on the host (created on first run) so a container's mutable state
197
+ survives across runs. Both are validated at pack time.
176
198
 
177
199
  Like everything else, these are reachable as `architect space pack|build|run`
178
200
  from inside a project.
@@ -280,7 +302,7 @@ architect evidence <it> --lane <lane> # transcribe the builder's report ve
280
302
  architect gate <iteration> # run the frozen gate commands, stream raw output
281
303
  architect merge <it> <lane> # integrate ONE judged-passing lane (--no-ff)
282
304
  architect integrate <it> --lanes a,b # integrate a set of passing lanes, in order
283
- architect land # end-of-project PR command (no push, no gh)
305
+ # landing (PR body + push/PR command) is the architect's end-of-project procedure (see skill)
284
306
  architect status # project state (read-only)
285
307
  architect variant add|compare|promote … # competing (harness, model) lanes over one frozen spec
286
308
  architect research dispatch|status|wait … # parallel read-only research lanes (see below)
@@ -299,7 +321,7 @@ architect evidence my-feature --lane lane-a # transcribe raw evidence
299
321
  architect gate my-feature # run the frozen gates yourself
300
322
  # … read the diff against the spec, then write the Verdict …
301
323
  architect integrate my-feature --lanes lane-a # merge passing lanes → project/<slug>
302
- architect land # print gh pr create at project end
324
+ # landing (PR body + push/PR command) is the architect's end-of-project procedure (see skill)
303
325
  ```
304
326
 
305
327
  ### Streaming builder output 📡
@@ -415,43 +415,6 @@ module Space::Architect
415
415
  merged
416
416
  end
417
417
 
418
- # Generate the end-of-project PR command(s) for each integrated repo.
419
- # Writes a PR body to build/land/<repo>-pr-body.md and returns, per repo:
420
- # { repo:, integration_branch:, body_file:, command:, context: }.
421
- # Raises Space::Core::Error if nothing has been integrated yet.
422
- # Side-effect-free: no git write, no push, no gh.
423
- def land
424
- b = space.data["project"] || {}
425
- integration_branch = project_integration_branch
426
-
427
- integrated_lanes = (b["iterations"] || []).flat_map do |s|
428
- (s["lanes"] || []).filter_map { |l| { iteration: s, lane: l } if l["integration_branch"] }
429
- end
430
-
431
- raise Space::Core::Error, "nothing integrated yet — integrate a lane before landing" if integrated_lanes.empty?
432
-
433
- repos = integrated_lanes.map { |e| e[:lane]["repo"] }.uniq
434
-
435
- repos.map do |repo|
436
- body_dir = space.path.join("build", "land")
437
- FileUtils.mkdir_p(body_dir)
438
- body_path = body_dir.join("#{repo}-pr-body.md")
439
-
440
- iterations = b["iterations"] || []
441
- body = +"# #{space.title}\n\nMerges `#{integration_branch}` → `main`.\n\n## Iterations\n\n"
442
- iterations.each do |s|
443
- nn = format("%02d", s["ordinal"])
444
- verdict = s["verdict"] || "—"
445
- body << "- I#{nn} #{s["name"]} — #{verdict}\n"
446
- end
447
- body_path.write(body)
448
-
449
- cmd = %(gh pr create --base main --head #{integration_branch} --title "#{space.title}" --body-file #{body_path})
450
- context = "# Run from repos/#{repo} on branch #{integration_branch} (gh pushes it)"
451
- { repo: repo, integration_branch: integration_branch, body_file: body_path.to_s, command: cmd, context: context }
452
- end
453
- end
454
-
455
418
  # Run the iteration's frozen Acceptance Criteria gate commands. Each gate is
456
419
  # executed in the resolved cwd (per-gate `cwd` overrides the base dir), under
457
420
  # a hard timeout, and evaluated against its `expect` block. Returns an array
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "pathname"
5
+
6
+ module Space
7
+ module Architect
8
+ module BugReport
9
+ REPO = "jetpks/space-architect"
10
+
11
+ class << self
12
+ def generate(space: nil, env: ENV, cwd: Dir.pwd, now: Time.now)
13
+ body_path = resolve_body_path(space, cwd, now)
14
+ FileUtils.mkdir_p(body_path.dirname)
15
+ body = build_body(space)
16
+ body_path.write(body)
17
+ contracted = Space::Core::Paths.contract(body_path, env: env)
18
+ command = Space::Core::Commands.wrap(
19
+ %(gh issue create -R #{REPO} --title "<one-line summary>" --body-file #{contracted})
20
+ )
21
+ { body_path: body_path, command: command, body: body }
22
+ end
23
+
24
+ private
25
+
26
+ def resolve_body_path(space, cwd, now)
27
+ filename = "architect-bug-report-#{now.strftime('%Y%m%d-%H%M%S')}.md"
28
+ if space
29
+ space.path.join("build", "bug-report", filename)
30
+ else
31
+ Pathname.new(cwd).join(filename)
32
+ end
33
+ end
34
+
35
+ def build_body(space)
36
+ body = +template_header
37
+ body << diagnostics_section
38
+ body << space_section(space) if space
39
+ body
40
+ end
41
+
42
+ def template_header
43
+ <<~MD
44
+ <!-- Title: <one-line summary> -->
45
+
46
+ **Kind:** <!-- process / tooling / both -->
47
+
48
+ ## Summary
49
+
50
+ <!-- One sentence describing the bug. -->
51
+
52
+ ## What happened
53
+
54
+ <!-- Describe what you observed. -->
55
+
56
+ ## What was expected
57
+
58
+ <!-- Describe what you expected to happen. -->
59
+
60
+ ## Repro steps
61
+
62
+ <!-- Numbered steps to reproduce. -->
63
+
64
+ MD
65
+ end
66
+
67
+ def diagnostics_section
68
+ <<~MD
69
+ ## Diagnostics
70
+
71
+ - space-architect: #{Space::Core::VERSION}
72
+ - ruby: #{RUBY_VERSION} (#{RUBY_PLATFORM})
73
+ MD
74
+ end
75
+
76
+ def space_section(space)
77
+ iterations = Array(space.data.dig("project", "iterations"))
78
+ iter_lines = iterations.map do |s|
79
+ nn = format("%02d", s["ordinal"])
80
+ verdict = s["verdict"] || "—"
81
+ "- I#{nn} #{s["name"]} — #{verdict}"
82
+ end.join("\n")
83
+
84
+ +"\n## Space context\n\n" \
85
+ "- Space id: #{space.id}\n" \
86
+ "- Space title: #{space.title}\n" \
87
+ "\n### Iterations\n\n" \
88
+ "#{iter_lines.empty? ? "(none)" : iter_lines}\n"
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
@@ -389,27 +389,6 @@ module Space::Architect
389
389
  end
390
390
  end
391
391
 
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)"
395
-
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
392
  class Gate < BaseCommand
414
393
  desc "Run the frozen Acceptance Criteria gate commands and report PASS/FAIL"
415
394
  argument :iteration, required: true, desc: "Iteration name"
@@ -439,6 +418,29 @@ module Space::Architect
439
418
  end
440
419
  end
441
420
 
421
+ class BugReport < BaseCommand
422
+ desc "Generate a prefilled GitHub issue template for filing bugs against space-architect"
423
+
424
+ def call(**opts)
425
+ setup_terminal(**opts.slice(:color, :colors))
426
+ handle_errors do
427
+ space = store.find.value_or(nil)
428
+ result = Space::Architect::BugReport.generate(
429
+ space: space,
430
+ env: project_config.env
431
+ )
432
+ terminal.say "Fill the placeholders in #{terminal.path(result[:body_path].to_s)}, then run:"
433
+ terminal.say result[:command]
434
+ terminal.say ""
435
+ terminal.say "Diagnostics:"
436
+ terminal.say " space-architect #{Space::Core::VERSION}"
437
+ terminal.say " ruby #{RUBY_VERSION} (#{RUBY_PLATFORM})"
438
+ terminal.say " space: #{space.id} — #{space.title}" if space
439
+ CLI.record_outcome(Outcome.new(exit_code: 0))
440
+ end
441
+ end
442
+ end
443
+
442
444
  class InstallSkills < BaseCommand
443
445
  desc "Install bundled skills (architect, architect-research, architect-vocabulary) for a harness"
444
446
  option :provider, default: "claude", desc: "Harness: claude, codex, opencode, pi"
@@ -652,9 +654,9 @@ Space::Architect::CLI::Registry.register "verdict", Space::Architect::CLI::Arc
652
654
  Space::Architect::CLI::Registry.register "evidence", Space::Architect::CLI::Architect::Evidence
653
655
  Space::Architect::CLI::Registry.register "merge", Space::Architect::CLI::Architect::Merge
654
656
  Space::Architect::CLI::Registry.register "integrate", Space::Architect::CLI::Architect::Integrate
655
- Space::Architect::CLI::Registry.register "land", Space::Architect::CLI::Architect::Land
656
657
  Space::Architect::CLI::Registry.register "gate", Space::Architect::CLI::Architect::Gate
657
658
  Space::Architect::CLI::Registry.register "install-skills", Space::Architect::CLI::Architect::InstallSkills
659
+ Space::Architect::CLI::Registry.register "bug-report", Space::Architect::CLI::Architect::BugReport
658
660
  Space::Architect::CLI::Registry.register "brief" do |b|
659
661
  b.register "new", Space::Architect::CLI::Architect::Brief::New
660
662
  end
@@ -10,5 +10,6 @@ require_relative "space_architect/gate_lint"
10
10
  require_relative "space_architect/gate_evaluator"
11
11
  require_relative "space_architect/architect_project"
12
12
  require_relative "space_architect/skill_installer"
13
+ require_relative "space_architect/bug_report"
13
14
  require_relative "space_architect/research"
14
15
  require_relative "space_architect/cli"
@@ -9,21 +9,35 @@ class Run < BaseCommand
9
9
 
10
10
  argument :command, type: :array, required: false, desc: "Command to run in the container (default: login shell)"
11
11
  option :tty, type: :boolean, default: nil, desc: "Force interactive TTY (default: auto-detect)"
12
+ option :env, type: :array, desc: "Host env var to forward into the container (repeatable; adds to run.env)"
12
13
 
13
- def call(command: [], tty: nil, **opts)
14
+ def call(command: [], tty: nil, env: [], **opts)
14
15
  setup_terminal(**opts.slice(:color, :colors))
15
16
  handle_errors do
16
17
  result = store.current.bind do |space|
17
- runner = Space::Core::OciRunner.new(space: space, interactive: tty.nil? ? CLI.tty?(out) : tty)
18
+ runner = Space::Core::OciRunner.new(
19
+ space: space, interactive: tty.nil? ? CLI.tty?(out) : tty, env_vars: env
20
+ )
18
21
  runner.command(command).fmap { |argv| { argv: argv, runner: runner } }
19
22
  end
20
23
  render(result) do |r|
21
24
  r[:runner].host_dirs.each { |d| FileUtils.mkdir_p(d) }
25
+ warn_missing_env(r[:runner].missing_env)
22
26
  terminal.say "Running: #{r[:argv].join(' ')}"
23
27
  out.flush # Kernel.exec replaces the process without flushing buffered IO
24
28
  Kernel.exec(*r[:argv])
25
29
  end
26
30
  end
27
31
  end
32
+
33
+ private
34
+
35
+ # A requested var absent from the host env is almost always a forgotten export;
36
+ # forwarding silently omits it and the payload fails opaquely in-guest. Warn, don't fail.
37
+ def warn_missing_env(missing)
38
+ return if missing.empty?
39
+
40
+ terminal.error("Warning: requested env not set on host, not forwarded: #{missing.join(', ')}")
41
+ end
28
42
  end
29
43
  end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Space::Core
4
+ module Commands
5
+ module_function
6
+
7
+ # Render a multi-flag command with trailing " \" continuations broken at
8
+ # "--flag" boundaries, continuation lines indented two spaces. Commands
9
+ # without "--" flags are returned unchanged.
10
+ def wrap(command)
11
+ parts = command.split(/(?= --)/)
12
+ return command if parts.size <= 1
13
+
14
+ parts.each_with_index.map do |part, i|
15
+ segment = i.zero? ? part.rstrip : " #{part.lstrip}"
16
+ i < parts.size - 1 ? "#{segment} \\" : segment
17
+ end.join("\n")
18
+ end
19
+ end
20
+ end
@@ -84,6 +84,10 @@ module Space::Core
84
84
  [entrypoint_content].pack("m0")
85
85
  end
86
86
 
87
+ def local_space_architect?
88
+ space.path.join("repos/space-architect").directory?
89
+ end
90
+
87
91
  def render_template(name)
88
92
  template_path = TEMPLATE_DIR.join(name)
89
93
  ERB.new(template_path.read, trim_mode: "-").result(binding)
@@ -7,12 +7,15 @@ module Space::Core
7
7
  class OciRunner
8
8
  include Dry::Monads[:result]
9
9
 
10
+ # Always-forwarded substrate auth (the in-image architect/claude reach for these).
11
+ # Payload-specific vars are declared per-space via `run.env:` or the --env flag.
10
12
  AUTH_ENV = %w[ANTHROPIC_API_KEY CLAUDE_CODE_OAUTH_TOKEN ANTHROPIC_BASE_URL].freeze
11
13
 
12
- def initialize(space:, env: ENV.to_h, interactive: true)
14
+ def initialize(space:, env: ENV.to_h, interactive: true, env_vars: [])
13
15
  @space = space
14
16
  @env = env
15
17
  @interactive = interactive
18
+ @env_vars = (space.run_env + Array(env_vars)).map(&:to_s)
16
19
  end
17
20
 
18
21
  def image
@@ -36,7 +39,7 @@ module Space::Core
36
39
  argv = [
37
40
  "container", "run", "--rm",
38
41
  *(@interactive ? ["-i", "-t"] : []),
39
- *auth_flags,
42
+ *env_flags,
40
43
  *mount_flags,
41
44
  image,
42
45
  *extra
@@ -44,17 +47,30 @@ module Space::Core
44
47
  Success(argv)
45
48
  end
46
49
 
50
+ # Explicitly-requested vars (run.env: + --env) that are unset/empty in the host
51
+ # env, so the caller can warn — silence here would let a missing key fail opaquely
52
+ # inside the guest. AUTH_ENV is opportunistic and intentionally excluded.
53
+ def missing_env
54
+ @env_vars.uniq.reject { |var| present?(var) }
55
+ end
56
+
47
57
  private
48
58
 
49
59
  attr_reader :space, :env, :interactive
50
60
 
51
- def auth_flags
52
- AUTH_ENV.each_with_object([]) do |var, flags|
53
- val = env[var]
54
- flags.push("-e", var) if val && !val.empty?
61
+ # Bare `-e VAR` passthrough (no =value on the command line, so the secret never
62
+ # lands in argv/ps): substrate auth first, then the space's declared payload vars.
63
+ def env_flags
64
+ (AUTH_ENV + @env_vars).uniq.each_with_object([]) do |var, flags|
65
+ flags.push("-e", var) if present?(var)
55
66
  end
56
67
  end
57
68
 
69
+ def present?(var)
70
+ val = env[var]
71
+ val && !val.empty?
72
+ end
73
+
58
74
  def mount_flags
59
75
  mounts.each_with_object([]) do |(host, guest), flags|
60
76
  flags.push("-v", "#{host}:#{guest}")
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Space::Core
4
+ module Paths
5
+ module_function
6
+
7
+ def contract(path, env: ENV)
8
+ value = path.to_s
9
+ home = XDG.home(env: env)
10
+ [home, realpath_or_nil(home)].compact.uniq.each do |h|
11
+ return "~" if value == h
12
+ return "~#{value.delete_prefix(h)}" if value.start_with?("#{h}/")
13
+ end
14
+ value
15
+ end
16
+
17
+ def realpath_or_nil(path)
18
+ File.realpath(path)
19
+ rescue SystemCallError
20
+ nil
21
+ end
22
+ end
23
+ end
@@ -56,6 +56,12 @@ module Space::Core
56
56
  Array(data.dig("pack", "persist")).map(&:to_s)
57
57
  end
58
58
 
59
+ # Host env vars to forward into the container at `space run` (bare passthrough).
60
+ # Declared per-space so payloads needing credentials run without a wall of flags.
61
+ def run_env
62
+ Array(data.dig("run", "env")).map(&:to_s)
63
+ end
64
+
59
65
  def architect
60
66
  data["project"]
61
67
  end
@@ -30,18 +30,6 @@ RUN apt-get update -qq \
30
30
  # Claude Code CLI (https://claude.ai/install.sh)
31
31
  RUN curl -fsSL https://claude.ai/install.sh | bash
32
32
 
33
- # Copy the space tree into the image (filtered by Dockerfile.dockerignore alongside this Dockerfile)
34
- COPY . /space
35
-
36
- # Install space-architect gem: prefer in-space checkout for a pinned version, fall back to RubyGems
37
- RUN if [ -d /space/repos/space-architect ]; then \
38
- cd /space/repos/space-architect \
39
- && rake build \
40
- && gem install --no-document pkg/*.gem; \
41
- else \
42
- gem install --no-document space-architect; \
43
- fi
44
-
45
33
  # Write entrypoint: sets git safe.directory, execs requested command or a login shell
46
34
  RUN echo '<%= entrypoint_b64 %>' | base64 -d > /entrypoint.sh \
47
35
  && chmod +x /entrypoint.sh
@@ -52,10 +40,25 @@ RUN printf 'export PATH="/usr/local/bundle/bin:/root/.local/bin:$PATH"\n' \
52
40
  <% if provision_scripts.any? -%>
53
41
 
54
42
  # Space-declared build-time provisioning (pack.provision)
43
+ # Each script is COPY'd individually — a space-content edit elsewhere does not bust its cache layer.
55
44
  <% provision_scripts.each do |script| -%>
45
+ COPY <%= script %> /space/<%= script %>
56
46
  RUN /space/<%= script %>
57
47
  <% end -%>
58
48
  <% end -%>
49
+
50
+ # Install space-architect gem
51
+ <% if local_space_architect? -%>
52
+ COPY repos/space-architect /space/repos/space-architect
53
+ RUN cd /space/repos/space-architect \
54
+ && rake build \
55
+ && gem install --no-document pkg/*.gem
56
+ <% else -%>
57
+ RUN gem install --no-document space-architect
58
+ <% end -%>
59
+
60
+ # Copy the space tree into the image (filtered by Dockerfile.dockerignore alongside this Dockerfile)
61
+ COPY . /space
59
62
  <% if persist_paths.any? -%>
60
63
 
61
64
  # Seed snapshot: capture baked content of persisted dirs so a first-run (empty) mount is seeded from it
@@ -48,13 +48,7 @@ module Space::Core
48
48
  end
49
49
 
50
50
  def path(path)
51
- value = path.to_s
52
- homes.each do |home|
53
- return "~" if value == home
54
- return "~#{value.delete_prefix(home)}" if value.start_with?("#{home}/")
55
- end
56
-
57
- value
51
+ Paths.contract(path, env: config.env)
58
52
  end
59
53
 
60
54
  def with_spinner(message)
@@ -108,17 +102,6 @@ module Space::Core
108
102
  end
109
103
  end
110
104
 
111
- def homes
112
- home = XDG.home(env: config.env)
113
- [home, realpath_or_nil(home)].compact.uniq
114
- end
115
-
116
- def realpath_or_nil(path)
117
- File.realpath(path)
118
- rescue SystemCallError
119
- nil
120
- end
121
-
122
105
  def table_row(headers, row, column_widths, header: false)
123
106
  row.each_with_index.map do |cell, index|
124
107
  raw = cell.to_s
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Space
4
4
  module Core
5
- VERSION = "2.0.1"
5
+ VERSION = "3.0.0"
6
6
  end
7
7
  end
data/lib/space_core.rb CHANGED
@@ -10,6 +10,8 @@ require "space_core/warnings"
10
10
  Space::Core::Warnings.disable_experimental!
11
11
  require "space_core/atomic_write"
12
12
  require "space_core/xdg"
13
+ require "space_core/paths"
14
+ require "space_core/commands"
13
15
  require "space_core/config"
14
16
  require "space_core/state"
15
17
  require "space_core/slugger"
@@ -110,6 +110,7 @@ loop.
110
110
  8. **Stop conditions:** failing verification you can't root-cause, instructions
111
111
  conflicting with project docs, irreversible/destructive calls, or scope
112
112
  growth beyond the iteration → checkpoint to the handoff and ask the human.
113
+ For bugs in the architect tooling or process itself, run `architect bug-report` — it prints a prefilled issue template and the `gh` command to file it.
113
114
 
114
115
  ## Procedure
115
116
 
@@ -373,10 +374,15 @@ cited BRIEF §sections (per §2), then write the **Verdict** (`architect verdict
373
374
  <iteration> continue|kill --from <file>`): disagreement rulings, per-AC
374
375
  PASS/FAIL/INVALID, the KILL/CONTINUE call.
375
376
 
376
- At project end, `architect land` prints the single `gh pr create --base main
377
- --head project/<slug>` command per touched repo and writes a PR body to
378
- `build/land/`no push, no `gh` call; the human runs it from the repo when
379
- the project is ready to ship.
377
+ At project end, landing is yours, not the CLI's the PR body is judgment
378
+ output, the same class as a Verdict. Per touched repo: write the PR body
379
+ yourselffrom the iteration verdicts, the integrated diff, and the BRIEF
380
+ to `build/land/<repo>-pr-body.md`, then present the paste-and-run block to the
381
+ human: `cd` to the repo checkout, `git push -u origin project/<slug>`, and
382
+ `gh pr create --base main --head project/<slug> --title … --body-file …` —
383
+ paths `~`-contracted, the multi-flag command broken with trailing ` \` at flag
384
+ boundaries. No push, no `gh` call by you or the CLI; the human runs the block
385
+ when the project is ready to ship.
380
386
 
381
387
  ## Maintenance
382
388
 
@@ -112,7 +112,9 @@ lanes pass; the CLI does the git mechanics. Canonical path:
112
112
  architect integrate <iteration> --lanes <passing-set> # e.g. --lanes lane-a,lane-b
113
113
  architect gate <iteration> # integration smoke (raw output; verdict stays yours)
114
114
  architect integrate <iteration> --lanes <passing-set> --teardown # or remove worktrees + lane branches after
115
- architect land # end of project: prints gh pr create --base main --head project/<slug>
115
+ # end of project: landing is the architect's, not a CLI command — write the PR
116
+ # body to build/land/<repo>-pr-body.md yourself, then present the paste-and-run
117
+ # block (cd, git push -u origin project/<slug>, gh pr create) — see SKILL.md §6
116
118
  ```
117
119
 
118
120
  `architect integrate` commits each named lane on its branch and merges it
@@ -138,8 +140,8 @@ git -C repos/<repo> merge --no-ff lane/<iteration>-<lane>
138
140
  <run the gate commands> # integration smoke after every merge
139
141
  architect worktree remove <iteration> <lane>
140
142
  git -C repos/<repo> branch -d lane/<iteration>-<lane>
141
- # at project end:
142
- architect land # prints gh pr create --base main --head project/<slug>
143
+ # at project end there is no CLI step: the architect writes
144
+ # build/land/<repo>-pr-body.md and presents the push + gh pr create block
143
145
  ```
144
146
 
145
147
  ### Parallel + fast-follow
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: space-architect
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.1
4
+ version: 3.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Eric Jacobs
@@ -240,6 +240,7 @@ files:
240
240
  - exe/src
241
241
  - lib/space_architect.rb
242
242
  - lib/space_architect/architect_project.rb
243
+ - lib/space_architect/bug_report.rb
243
244
  - lib/space_architect/cli.rb
244
245
  - lib/space_architect/cli/architect.rb
245
246
  - lib/space_architect/cli/research.rb
@@ -281,6 +282,7 @@ files:
281
282
  - lib/space_core/cli/show.rb
282
283
  - lib/space_core/cli/status.rb
283
284
  - lib/space_core/cli/use.rb
285
+ - lib/space_core/commands.rb
284
286
  - lib/space_core/config.rb
285
287
  - lib/space_core/errors.rb
286
288
  - lib/space_core/git_client.rb
@@ -288,6 +290,7 @@ files:
288
290
  - lib/space_core/oci_builder.rb
289
291
  - lib/space_core/oci_packer.rb
290
292
  - lib/space_core/oci_runner.rb
293
+ - lib/space_core/paths.rb
291
294
  - lib/space_core/repo_reference.rb
292
295
  - lib/space_core/repo_resolver.rb
293
296
  - lib/space_core/shell_integration.rb