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 +4 -4
- data/CHANGELOG.md +44 -0
- data/README.md +28 -6
- data/lib/space_architect/architect_project.rb +0 -37
- data/lib/space_architect/bug_report.rb +93 -0
- data/lib/space_architect/cli/architect.rb +24 -22
- data/lib/space_architect.rb +1 -0
- data/lib/space_core/cli/run.rb +16 -2
- data/lib/space_core/commands.rb +20 -0
- data/lib/space_core/oci_packer.rb +4 -0
- data/lib/space_core/oci_runner.rb +22 -6
- data/lib/space_core/paths.rb +23 -0
- data/lib/space_core/space.rb +6 -0
- data/lib/space_core/templates/oci/dockerfile.erb +15 -12
- data/lib/space_core/terminal.rb +1 -18
- data/lib/space_core/version.rb +1 -1
- data/lib/space_core.rb +2 -0
- data/skill/architect/SKILL.md +10 -4
- data/skill/architect/dispatch.md +5 -3
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 135a6282cfe3e3073265b71867e7d228673cbfb9b7253feaaba5ad67a180b7cb
|
|
4
|
+
data.tar.gz: 0d0406a99feadadebb24a6167c11837de25bede933e752ef6507524208f4e5a7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
|
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
|
-
|
|
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
|
data/lib/space_architect.rb
CHANGED
|
@@ -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"
|
data/lib/space_core/cli/run.rb
CHANGED
|
@@ -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(
|
|
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
|
-
*
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
data/lib/space_core/space.rb
CHANGED
|
@@ -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
|
data/lib/space_core/terminal.rb
CHANGED
|
@@ -48,13 +48,7 @@ module Space::Core
|
|
|
48
48
|
end
|
|
49
49
|
|
|
50
50
|
def path(path)
|
|
51
|
-
|
|
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
|
data/lib/space_core/version.rb
CHANGED
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"
|
data/skill/architect/SKILL.md
CHANGED
|
@@ -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,
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
the
|
|
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
|
+
yourself — from 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
|
|
data/skill/architect/dispatch.md
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|