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
@@ -0,0 +1,177 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/cli"
4
+ require "pastel"
5
+ require_relative "cli/repeatable_options"
6
+ require_relative "cli/help"
7
+
8
+ module Space::Core
9
+ module CLI
10
+ CLI = self
11
+
12
+ Outcome = Data.define(:exit_code, :message) do
13
+ def initialize(exit_code:, message: nil) = super
14
+ end
15
+
16
+ def self.record_outcome(o) = (Thread.current[:space_core_cli_outcome] = o)
17
+ def self.last_outcome = Thread.current[:space_core_cli_outcome]
18
+
19
+ # Pastel used by the colourful help listing (Help / the Usage reopen). Set
20
+ # per-invocation in .call from the output stream and --color; defaults to a
21
+ # disabled instance so non-CLI callers and tests render plain text.
22
+ def self.help_pastel = @help_pastel ||= Pastel.new(enabled: false)
23
+
24
+ def self.help_pastel=(pastel)
25
+ @help_pastel = pastel
26
+ end
27
+
28
+ # Whether the help listing should be colourised: honour an explicit
29
+ # --color/--colors (always/never), otherwise auto-detect from the streams the
30
+ # listing can land on (stdout for top-level help, stderr for bare namespaces).
31
+ def self.help_colors?(argv, out, err)
32
+ case color_mode(argv)
33
+ when "always" then true
34
+ when "never" then false
35
+ else tty?(out) || tty?(err)
36
+ end
37
+ end
38
+
39
+ def self.tty?(io) = io.respond_to?(:tty?) && io.tty?
40
+
41
+ def self.color_mode(argv)
42
+ argv.each_with_index do |arg, i|
43
+ return arg.split("=", 2)[1].to_s.downcase if arg.start_with?("--color=", "--colors=")
44
+ return argv[i + 1].to_s.downcase if %w[--color --colors].include?(arg)
45
+ end
46
+ "auto"
47
+ end
48
+
49
+ module Registry
50
+ extend Dry::CLI::Registry
51
+ end
52
+
53
+ TOP_LEVEL_HELP = [[], ["--help"], ["-h"], ["help"]].freeze
54
+ VERSION_REQUEST = [["version"], ["--version"]].freeze
55
+
56
+ def self.call(argv, out = $stdout, err = $stderr)
57
+ Thread.current[:space_core_cli_outcome] = nil
58
+ self.help_pastel = Pastel.new(enabled: help_colors?(argv, out, err))
59
+
60
+ if TOP_LEVEL_HELP.include?(argv)
61
+ print_usage(out)
62
+ return 0
63
+ end
64
+
65
+ if VERSION_REQUEST.include?(argv)
66
+ print_version(out)
67
+ return 0
68
+ end
69
+
70
+ Dry::CLI.new(Registry).call(arguments: normalize_args(argv), out: out, err: err)
71
+ last_outcome&.exit_code || 0
72
+ end
73
+
74
+ # Move --color/--colors options to the end of the argument list so dry-cli's
75
+ # command routing is not confused by options before the subcommand name.
76
+ #
77
+ # Two passes:
78
+ # 1. Leading: extract two-token form (--color VALUE) and =-form from the
79
+ # front while args still look like options.
80
+ # 2. Non-leading: extract =-form (--color=VALUE / --colors=VALUE) from any
81
+ # position before the -- separator. The bare two-token form is ambiguous
82
+ # with a subcommand name in non-leading position and is left in place.
83
+ def self.normalize_args(argv)
84
+ args = argv.dup
85
+ extracted = []
86
+
87
+ # Pass 1: leading two-token and =-form (existing behavior, unchanged)
88
+ while (arg = args.first) && arg != "--" && arg.start_with?("-")
89
+ if %w[--color --colors].include?(arg)
90
+ extracted << args.shift
91
+ extracted << args.shift if args.first && !args.first.start_with?("-")
92
+ elsif arg.start_with?("--color=", "--colors=")
93
+ extracted << args.shift
94
+ else
95
+ break
96
+ end
97
+ end
98
+
99
+ # Pass 2: =-form from any non-leading position, stop at --
100
+ sep = args.index("--")
101
+ head = sep ? args[0, sep] : args
102
+ tail = sep ? args[sep..] : []
103
+ mid_color, head = head.partition { |a| a.start_with?("--color=", "--colors=") }
104
+ extracted += mid_color
105
+ args = head + tail
106
+
107
+ extracted.empty? ? args : args + extracted
108
+ end
109
+
110
+ def self.print_usage(out)
111
+ out.puts Dry::CLI::Usage.call(Registry.get([]))
112
+ end
113
+
114
+ def self.print_version(out)
115
+ out.puts ::Space::Core::VERSION
116
+ end
117
+
118
+ def self.run(argv, out = $stdout, err = $stderr)
119
+ Kernel.exit(call(argv, out, err))
120
+ rescue Interrupt
121
+ err.puts "interrupted"
122
+ Kernel.exit(130)
123
+ end
124
+ end
125
+ end
126
+
127
+ require_relative "cli/helpers"
128
+ require_relative "cli/base_command"
129
+ require_relative "cli/init"
130
+ require_relative "cli/new"
131
+ require_relative "cli/list"
132
+ require_relative "cli/show"
133
+ require_relative "cli/path"
134
+ require_relative "cli/use"
135
+ require_relative "cli/current"
136
+ require_relative "cli/status"
137
+ require_relative "cli/config"
138
+ require_relative "cli/repo"
139
+ require_relative "cli/shell"
140
+ require_relative "cli/pack"
141
+ require_relative "cli/build"
142
+ require_relative "cli/run"
143
+
144
+ Space::Core::CLI::Registry.register "init", Space::Core::CLI::Init
145
+ Space::Core::CLI::Registry.register "new", Space::Core::CLI::New
146
+ Space::Core::CLI::Registry.register "list", Space::Core::CLI::List
147
+ Space::Core::CLI::Registry.register "ls", Space::Core::CLI::List
148
+ Space::Core::CLI::Registry.register "show", Space::Core::CLI::Show
149
+ Space::Core::CLI::Registry.register "path", Space::Core::CLI::Path
150
+ Space::Core::CLI::Registry.register "use", Space::Core::CLI::Use
151
+ Space::Core::CLI::Registry.register "current", Space::Core::CLI::Current
152
+ Space::Core::CLI::Registry.register "status", Space::Core::CLI::Status
153
+ Space::Core::CLI::Registry.register "config" do |c|
154
+ c.register "show", Space::Core::CLI::Config::Show
155
+ c.register "path", Space::Core::CLI::Config::ConfigPath
156
+ c.register "set", Space::Core::CLI::Config::Set
157
+ end
158
+ Space::Core::CLI::Registry.register "repo" do |r|
159
+ r.register "add", Space::Core::CLI::Repo::Add
160
+ r.register "list", Space::Core::CLI::Repo::RepoList
161
+ r.register "ls", Space::Core::CLI::Repo::RepoList
162
+ r.register "resolve", Space::Core::CLI::Repo::Resolve
163
+ end
164
+ Space::Core::CLI::Registry.register "repos" do |r|
165
+ r.register "add", Space::Core::CLI::Repo::Add
166
+ r.register "list", Space::Core::CLI::Repo::RepoList
167
+ r.register "ls", Space::Core::CLI::Repo::RepoList
168
+ r.register "resolve", Space::Core::CLI::Repo::Resolve
169
+ end
170
+ Space::Core::CLI::Registry.register "shell" do |s|
171
+ s.register "init", Space::Core::CLI::Shell::ShellInit
172
+ s.register "fish", Space::Core::CLI::Shell::Fish
173
+ s.register "complete", Space::Core::CLI::Shell::Complete
174
+ end
175
+ Space::Core::CLI::Registry.register "pack", Space::Core::CLI::Pack
176
+ Space::Core::CLI::Registry.register "build", Space::Core::CLI::Build
177
+ Space::Core::CLI::Registry.register "run", Space::Core::CLI::Run
@@ -3,7 +3,7 @@
3
3
  require "yaml"
4
4
  require "pathname"
5
5
 
6
- module SpaceArchitect
6
+ module Space::Core
7
7
  class Config
8
8
  DEFAULT_DATA = {
9
9
  "version" => 1,
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module SpaceArchitect
3
+ module Space::Core
4
4
  class Error < StandardError; end
5
5
  class NotFoundError < Error; end
6
6
  class AmbiguousSpaceError < Error; end
@@ -5,7 +5,7 @@ require "async/process"
5
5
  require "pathname"
6
6
  require "tempfile"
7
7
 
8
- module SpaceArchitect
8
+ module Space::Core
9
9
  class GitClient
10
10
  def init(path)
11
11
  path = Pathname.new(path)
@@ -4,7 +4,7 @@ require "async/process"
4
4
  require "pathname"
5
5
  require "tempfile"
6
6
 
7
- module SpaceArchitect
7
+ module Space::Core
8
8
  class MiseClient
9
9
  def trust(path)
10
10
  path = Pathname.new(path)
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+ require "pathname"
5
+ require "dry/monads"
6
+
7
+ module Space::Core
8
+ class OciBuilder
9
+ include Dry::Monads[:result]
10
+
11
+ def initialize(space:, output_dir:)
12
+ @space = space
13
+ @output_dir = Pathname.new(output_dir)
14
+ end
15
+
16
+ def version
17
+ compute_version.value!
18
+ end
19
+
20
+ def image
21
+ "#{space.id}:#{version}"
22
+ end
23
+
24
+ def latest
25
+ "#{space.id}:latest"
26
+ end
27
+
28
+ def command
29
+ compute_version.fmap do |ver|
30
+ [
31
+ "container", "build",
32
+ "-f", @output_dir.join("Dockerfile").to_s,
33
+ "-t", "#{space.id}:#{ver}",
34
+ "-t", latest,
35
+ space.path.to_s
36
+ ]
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ attr_reader :space
43
+
44
+ def compute_version
45
+ sha_out, _, sha_status = Open3.capture3("git", "-C", space.path.to_s, "rev-parse", "--short=12", "HEAD")
46
+ unless sha_status.success?
47
+ return Failure("space is not a git repository with a commit; cannot compute a version tag")
48
+ end
49
+
50
+ sha = sha_out.strip
51
+ status_out, _, _ = Open3.capture3("git", "-C", space.path.to_s, "status", "--porcelain")
52
+ dirty = !status_out.strip.empty?
53
+ Success(dirty ? "#{sha}-dirty" : sha)
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "erb"
4
+ require "fileutils"
5
+ require "pathname"
6
+ require "dry/monads"
7
+
8
+ module Space::Core
9
+ class OciPacker
10
+ include Dry::Monads[:result]
11
+
12
+ TEMPLATE_DIR = Pathname.new(__dir__).join("templates", "oci").freeze
13
+
14
+ def initialize(space:, output_dir:)
15
+ @space = space
16
+ @output_dir = Pathname.new(output_dir)
17
+ end
18
+
19
+ def generate
20
+ FileUtils.mkdir_p(@output_dir)
21
+ validated = validate_provision_scripts(space.provision_scripts)
22
+ return validated if validated.failure?
23
+ validated = validate_persist_paths(space.persist_paths)
24
+ return validated if validated.failure?
25
+ write("Dockerfile", render_template("dockerfile.erb"))
26
+ write("entrypoint.sh", entrypoint_content, mode: 0o755)
27
+ write("Dockerfile.dockerignore", render_template("dockerignore.erb"))
28
+ Success(@output_dir)
29
+ rescue StandardError => e
30
+ Failure(e)
31
+ end
32
+
33
+ private
34
+
35
+ attr_reader :space, :output_dir
36
+
37
+ def space_id
38
+ @space.id
39
+ end
40
+
41
+ def repos
42
+ @space.repos
43
+ end
44
+
45
+ def provision_scripts
46
+ @space.provision_scripts
47
+ end
48
+
49
+ def persist_paths
50
+ @space.persist_paths
51
+ end
52
+
53
+ def validate_provision_scripts(scripts)
54
+ scripts.each do |rel_path|
55
+ if Pathname.new(rel_path).absolute?
56
+ return Failure("provision script '#{rel_path}' must not be an absolute path")
57
+ end
58
+
59
+ resolved = space.path.join(rel_path).cleanpath
60
+ space_root = space.path.cleanpath
61
+ unless resolved.to_s.start_with?("#{space_root}/") || resolved == space_root
62
+ return Failure("provision script '#{rel_path}' escapes the space root")
63
+ end
64
+
65
+ return Failure("provision script '#{rel_path}' does not exist under the space root") unless resolved.exist?
66
+ end
67
+ Success(scripts)
68
+ end
69
+
70
+ def validate_persist_paths(paths)
71
+ paths.each do |path|
72
+ unless Pathname.new(path).absolute?
73
+ return Failure("persist path '#{path}' must be an absolute path")
74
+ end
75
+ end
76
+ Success(paths)
77
+ end
78
+
79
+ def entrypoint_content
80
+ @entrypoint_content ||= render_template("entrypoint.sh.erb")
81
+ end
82
+
83
+ def entrypoint_b64
84
+ [entrypoint_content].pack("m0")
85
+ end
86
+
87
+ def render_template(name)
88
+ template_path = TEMPLATE_DIR.join(name)
89
+ ERB.new(template_path.read, trim_mode: "-").result(binding)
90
+ end
91
+
92
+ def write(filename, content, mode: 0o644)
93
+ path = @output_dir.join(filename)
94
+ File.write(path, content)
95
+ File.chmod(mode, path)
96
+ path
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+ require "dry/monads"
5
+
6
+ module Space::Core
7
+ class OciRunner
8
+ include Dry::Monads[:result]
9
+
10
+ AUTH_ENV = %w[ANTHROPIC_API_KEY CLAUDE_CODE_OAUTH_TOKEN ANTHROPIC_BASE_URL].freeze
11
+
12
+ def initialize(space:, env: ENV.to_h, interactive: true)
13
+ @space = space
14
+ @env = env
15
+ @interactive = interactive
16
+ end
17
+
18
+ def image
19
+ "#{space.id}:latest"
20
+ end
21
+
22
+ def mounts
23
+ space.persist_paths.map do |guest|
24
+ [space.path.join(".state" + guest), guest]
25
+ end
26
+ end
27
+
28
+ def host_dirs
29
+ mounts.map(&:first)
30
+ end
31
+
32
+ def command(extra = [])
33
+ validated = validate_persist_paths(space.persist_paths)
34
+ return validated if validated.failure?
35
+
36
+ argv = [
37
+ "container", "run", "--rm",
38
+ *(@interactive ? ["-i", "-t"] : []),
39
+ *auth_flags,
40
+ *mount_flags,
41
+ image,
42
+ *extra
43
+ ]
44
+ Success(argv)
45
+ end
46
+
47
+ private
48
+
49
+ attr_reader :space, :env, :interactive
50
+
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?
55
+ end
56
+ end
57
+
58
+ def mount_flags
59
+ mounts.each_with_object([]) do |(host, guest), flags|
60
+ flags.push("-v", "#{host}:#{guest}")
61
+ end
62
+ end
63
+
64
+ def validate_persist_paths(paths)
65
+ paths.each do |path|
66
+ unless Pathname.new(path).absolute?
67
+ return Failure("persist path '#{path}' must be an absolute path")
68
+ end
69
+ end
70
+ Success(paths)
71
+ end
72
+ end
73
+ end
@@ -2,7 +2,7 @@
2
2
 
3
3
  require "pathname"
4
4
 
5
- module SpaceArchitect
5
+ module Space::Core
6
6
  RepoReference = Data.define(:provider, :owner, :name, :clone_url, :source) do
7
7
  def full_name
8
8
  "#{provider}/#{owner}/#{name}"
@@ -2,7 +2,7 @@
2
2
 
3
3
  require "uri"
4
4
 
5
- module SpaceArchitect
5
+ module Space::Core
6
6
  class RepoResolver
7
7
  SCP_LIKE_PATTERN = /\A(?:[^@\/]+@)?(?<provider>[^:\/]+):(?<path>.+)\z/
8
8
  URL_PATTERN = %r{\A[A-Za-z][A-Za-z0-9+\-.]*://}
@@ -2,7 +2,7 @@
2
2
 
3
3
  require "fileutils"
4
4
 
5
- module SpaceArchitect
5
+ module Space::Core
6
6
  module ShellIntegration
7
7
  FISH_TEMPLATE = <<~'FISH'
8
8
  # Generated by space-architect. Do not edit by hand.
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module SpaceArchitect
3
+ module Space::Core
4
4
  module Slugger
5
5
  module_function
6
6
 
@@ -4,7 +4,7 @@ require "yaml"
4
4
  require "pathname"
5
5
  require "time"
6
6
 
7
- module SpaceArchitect
7
+ module Space::Core
8
8
  class Space
9
9
  METADATA_FILE = "space.yaml"
10
10
  VALID_STATUSES = %w[active paused done archived].freeze
@@ -48,12 +48,20 @@ module SpaceArchitect
48
48
  end
49
49
  end
50
50
 
51
+ def provision_scripts
52
+ Array(data.dig("pack", "provision")).map(&:to_s)
53
+ end
54
+
55
+ def persist_paths
56
+ Array(data.dig("pack", "persist")).map(&:to_s)
57
+ end
58
+
51
59
  def architect
52
- data["architect"]
60
+ data["project"]
53
61
  end
54
62
 
55
63
  def architect=(val)
56
- data["architect"] = val
64
+ data["project"] = val
57
65
  end
58
66
 
59
67
  def metadata_path
@@ -6,10 +6,10 @@ require "async/semaphore"
6
6
  require "pathname"
7
7
  require "time"
8
8
  require "dry/monads"
9
- require "space_architect/pristine/scm/git"
10
- require "space_architect/pristine/cloner"
9
+ require "space_src/scm/git"
10
+ require "space_src/cloner"
11
11
 
12
- module SpaceArchitect
12
+ module Space::Core
13
13
  class SpaceStore
14
14
  include Dry::Monads[:result, :maybe]
15
15
 
@@ -46,7 +46,7 @@ module SpaceArchitect
46
46
  init_git(path:, id:, git_client:) if git
47
47
  state.touch_recent(id)
48
48
  Success(space)
49
- rescue SpaceArchitect::Error => e
49
+ rescue Error => e
50
50
  Failure(e)
51
51
  end
52
52
 
@@ -67,7 +67,7 @@ module SpaceArchitect
67
67
  if looks_like_path?(value)
68
68
  begin
69
69
  return Success(Space.load(File.expand_path(value)))
70
- rescue SpaceArchitect::Error => e
70
+ rescue Error => e
71
71
  return Failure(e)
72
72
  end
73
73
  end
@@ -80,13 +80,13 @@ module SpaceArchitect
80
80
  end
81
81
 
82
82
  Failure(AmbiguousSpaceError.new("Space '#{value}' is ambiguous: #{matches.map(&:id).join(', ')}"))
83
- rescue SpaceArchitect::Error => e
83
+ rescue Error => e
84
84
  Failure(e)
85
85
  end
86
86
 
87
87
  def current(from: Dir.pwd)
88
88
  current_from_pwd(from:).to_result(CurrentSpaceMissingError.new("No current space found from #{from}. Run this inside a space or pass a space id."))
89
- rescue SpaceArchitect::Error => e
89
+ rescue Error => e
90
90
  Failure(e)
91
91
  end
92
92
 
@@ -112,15 +112,15 @@ module SpaceArchitect
112
112
  find(identifier).fmap { |space| state.touch_recent(space.id); space }
113
113
  end
114
114
 
115
- def add_repo(spec, from: Dir.pwd, scm: Pristine::SCM::Git.new, cloner: nil, mise_client: MiseClient.new)
115
+ def add_repo(spec, from: Dir.pwd, scm: ::Space::Src::SCM::Git.new, cloner: nil, mise_client: MiseClient.new)
116
116
  add_repos([spec], from:, scm:, cloner:, mise_client:).fmap(&:first)
117
117
  end
118
118
 
119
- def add_repos(specs, from: Dir.pwd, scm: Pristine::SCM::Git.new, cloner: nil, mise_client: MiseClient.new, reporter: nil)
119
+ def add_repos(specs, from: Dir.pwd, scm: ::Space::Src::SCM::Git.new, cloner: nil, mise_client: MiseClient.new, reporter: nil)
120
120
  current(from:).bind { |space| add_repos_to(space, specs, scm:, cloner:, mise_client:, reporter:) }
121
121
  end
122
122
 
123
- def add_repos_to(space, specs, scm: Pristine::SCM::Git.new, cloner: nil, mise_client: MiseClient.new, reporter: nil)
123
+ def add_repos_to(space, specs, scm: ::Space::Src::SCM::Git.new, cloner: nil, mise_client: MiseClient.new, reporter: nil)
124
124
  additions = prepare_repo_additions(space, specs)
125
125
  first_error = nil
126
126
 
@@ -148,7 +148,7 @@ module SpaceArchitect
148
148
  repo_data = space.add_repo(addition.fetch(:reference), relative_path: addition.fetch(:relative_path), now: now.call)
149
149
  { space: space, repo: repo_data, reference: addition.fetch(:reference), path: addition.fetch(:path) }
150
150
  end)
151
- rescue SpaceArchitect::Error => e
151
+ rescue Error => e
152
152
  Failure(e)
153
153
  end
154
154
 
@@ -178,7 +178,7 @@ module SpaceArchitect
178
178
  source = addition.fetch(:src_source)
179
179
 
180
180
  if source&.directory?
181
- actual_cloner = cloner || Pristine::Cloner.new(base_dir: config.src_dir)
181
+ actual_cloner = cloner || ::Space::Src::Cloner.new(base_dir: config.src_dir)
182
182
  result = actual_cloner.call(name: reference.full_name, into: destination.dirname.to_s)
183
183
  raise GitError, "clone failed (copy): #{result.failure}" if result.failure?
184
184
  else
@@ -257,7 +257,7 @@ module SpaceArchitect
257
257
  - `space.yaml` tracks the space identity, status, and associated metadata.
258
258
  - `repos/` contains cloned Git repositories for this work.
259
259
  - `notes/` is for task notes, scratch docs, and thinking-in-progress.
260
- - `architecture/` holds the architect mission memory (ARCHITECT.md and the per-iteration files).
260
+ - `architecture/` holds the architect project memory (ARCHITECT.md and the per-iteration files).
261
261
  - `tmp/` is the workspace-local scratch directory. Use it instead of `/tmp` or
262
262
  `/var/tmp`; when using `mktemp`, use `tmp/` as the base directory.
263
263
  - `build/` holds the architect loop's per-lane worktrees and scratch
@@ -3,7 +3,7 @@
3
3
  require "yaml"
4
4
  require "pathname"
5
5
 
6
- module SpaceArchitect
6
+ module Space::Core
7
7
  class State
8
8
  DEFAULT_DATA = {
9
9
  "version" => 1,
@@ -0,0 +1,63 @@
1
+ # Generated by `space pack` for space: <%= space_id %>
2
+ # Build (run from the space root):
3
+ # docker build -f <output_dir>/Dockerfile -t <%= space_id %>:latest .
4
+ #
5
+ # Run (auth injected at runtime — never baked into layers):
6
+ # container run --rm -it \
7
+ # -e ANTHROPIC_API_KEY=<key> \
8
+ <% persist_paths.each do |guest_path| -%>
9
+ # -v .state<%= guest_path %>:<%= guest_path %> \
10
+ <% end -%>
11
+ # <%= space_id %>:latest
12
+ <% if persist_paths.any? -%>
13
+ # (Persist: host paths are relative to space root; create them before first run.)
14
+ <% end -%>
15
+ #
16
+ # Available env vars at runtime:
17
+ # ANTHROPIC_API_KEY — API key
18
+ # CLAUDE_CODE_OAUTH_TOKEN — alternative to API key
19
+ # ANTHROPIC_BASE_URL — optional proxy endpoint
20
+
21
+ FROM ruby:4.0.5
22
+
23
+ RUN apt-get update -qq \
24
+ && apt-get install -y --no-install-recommends \
25
+ git \
26
+ curl \
27
+ ca-certificates \
28
+ && rm -rf /var/lib/apt/lists/*
29
+
30
+ # Claude Code CLI (https://claude.ai/install.sh)
31
+ RUN curl -fsSL https://claude.ai/install.sh | bash
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
+ # Write entrypoint: sets git safe.directory, execs requested command or a login shell
46
+ RUN echo '<%= entrypoint_b64 %>' | base64 -d > /entrypoint.sh \
47
+ && chmod +x /entrypoint.sh
48
+
49
+ # Prepend gem-exec and payload bin dirs so login shells (bash --login) resolve architect/space/claude
50
+ RUN printf 'export PATH="/usr/local/bundle/bin:/root/.local/bin:$PATH"\n' \
51
+ > /etc/profile.d/space-architect.sh
52
+ <% if provision_scripts.any? -%>
53
+
54
+ # Space-declared build-time provisioning (pack.provision)
55
+ <% provision_scripts.each do |script| -%>
56
+ RUN /space/<%= script %>
57
+ <% end -%>
58
+ <% end -%>
59
+
60
+ WORKDIR /space
61
+
62
+ ENTRYPOINT ["/entrypoint.sh"]
63
+ CMD ["bash", "--login"]