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,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "async/http/client"
4
+ require "async/http/endpoint"
5
+ require "json"
6
+
7
+ module Space::Architect
8
+ class RunCreator
9
+ def initialize(host, token, client: nil)
10
+ @host = host.chomp("/")
11
+ @token = token
12
+ @client = client
13
+ end
14
+
15
+ # POSTs to /runs and returns the integer run id.
16
+ # Raises Space::Core::Error on any failure — never returns nil.
17
+ def create
18
+ Sync do
19
+ if @client
20
+ response = @client.post("/runs", headers: headers, body: nil)
21
+ parse_response(response)
22
+ else
23
+ Async::HTTP::Client.open(Async::HTTP::Endpoint.parse(@host)) do |c|
24
+ response = c.post("/runs", headers: headers, body: nil)
25
+ parse_response(response)
26
+ end
27
+ end
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def headers
34
+ [
35
+ ["authorization", "Bearer #{@token}"],
36
+ ["content-type", "application/json"]
37
+ ]
38
+ end
39
+
40
+ def parse_response(response)
41
+ status = response.status
42
+ body = response.read || ""
43
+ raise Space::Core::Error, "POST /runs failed (#{status}): #{body[0, 200]}" unless status == 201
44
+
45
+ parsed = JSON.parse(body)
46
+ id = parsed["id"]
47
+ raise Space::Core::Error, "POST /runs: missing or non-integer id in response: #{body[0, 200]}" \
48
+ unless id.is_a?(Integer)
49
+
50
+ id
51
+ end
52
+ end
53
+ end
@@ -3,103 +3,105 @@
3
3
  require "fileutils"
4
4
  require "pathname"
5
5
 
6
- module SpaceArchitect
7
- module SkillInstaller
8
- PROVIDERS = %w[claude codex opencode pi].freeze
9
-
10
- class << self
11
- def source_root
12
- Pathname.new(__dir__).parent.parent.join("skill")
13
- end
14
-
15
- def dest_root(provider, project:, env:, cwd: Dir.pwd)
16
- case provider.to_s
17
- when "claude"
18
- base = project ? Pathname.new(cwd) : Pathname.new(XDG.home(env: env))
19
- base.join(".claude", "skills")
20
- when "codex"
21
- base = project ? Pathname.new(cwd) : Pathname.new(XDG.home(env: env))
22
- base.join(".agents", "skills")
23
- when "opencode"
24
- project ? Pathname.new(cwd).join(".opencode", "skills") : XDG.config_home(env: env).join("skills")
25
- when "pi"
26
- base = project ? Pathname.new(cwd) : Pathname.new(pi_agent_dir(env: env))
27
- base.join("skills")
28
- else
29
- raise Error, "Unknown provider '#{provider}'. Expected one of: #{PROVIDERS.join(', ')}"
6
+ module Space
7
+ module Architect
8
+ module SkillInstaller
9
+ PROVIDERS = %w[claude codex opencode pi].freeze
10
+
11
+ class << self
12
+ def source_root
13
+ Pathname.new(__dir__).parent.parent.join("skill")
30
14
  end
31
- end
32
-
33
- def install(provider, project:, force:, env:, cwd: Dir.pwd, dry_run: false)
34
- validate_provider!(provider)
35
- dest = dest_root(provider, project: project, env: env, cwd: cwd)
36
- results = []
37
15
 
38
- source_skills.each do |skill_dir|
39
- name = skill_dir.basename.to_s
40
- skill_dest = dest.join(name)
41
- results << install_skill(skill_dir, skill_dest, force: force, dry_run: dry_run)
16
+ def dest_root(provider, project:, env:, cwd: Dir.pwd)
17
+ case provider.to_s
18
+ when "claude"
19
+ base = project ? Pathname.new(cwd) : Pathname.new(Space::Core::XDG.home(env: env))
20
+ base.join(".claude", "skills")
21
+ when "codex"
22
+ base = project ? Pathname.new(cwd) : Pathname.new(Space::Core::XDG.home(env: env))
23
+ base.join(".agents", "skills")
24
+ when "opencode"
25
+ project ? Pathname.new(cwd).join(".opencode", "skills") : Space::Core::XDG.config_home(env: env).join("skills")
26
+ when "pi"
27
+ base = project ? Pathname.new(cwd) : Pathname.new(pi_agent_dir(env: env))
28
+ base.join("skills")
29
+ else
30
+ raise Space::Core::Error, "Unknown provider '#{provider}'. Expected one of: #{PROVIDERS.join(', ')}"
31
+ end
42
32
  end
43
33
 
44
- { dest_root: dest, skills: results, dry_run: dry_run }
45
- end
46
-
47
- def source_skills
48
- source_root.children.select(&:directory?)
49
- end
34
+ def install(provider, project:, force:, env:, cwd: Dir.pwd, dry_run: false)
35
+ validate_provider!(provider)
36
+ dest = dest_root(provider, project: project, env: env, cwd: cwd)
37
+ results = []
50
38
 
51
- private
52
-
53
- def validate_provider!(provider)
54
- return if PROVIDERS.include?(provider.to_s)
39
+ source_skills.each do |skill_dir|
40
+ name = skill_dir.basename.to_s
41
+ skill_dest = dest.join(name)
42
+ results << install_skill(skill_dir, skill_dest, force: force, dry_run: dry_run)
43
+ end
55
44
 
56
- raise Error, "Unknown provider '#{provider}'. Expected one of: #{PROVIDERS.join(', ')}"
57
- end
45
+ { dest_root: dest, skills: results, dry_run: dry_run }
46
+ end
58
47
 
59
- def pi_agent_dir(env:)
60
- Pathname.new(env.fetch("PI_CODING_AGENT_DIR", File.join(XDG.home(env: env), ".pi", "agent")))
61
- end
48
+ def source_skills
49
+ source_root.children.select(&:directory?)
50
+ end
62
51
 
63
- def install_skill(source, dest, force:, dry_run:)
64
- name = source.basename.to_s
52
+ private
65
53
 
66
- if dest.exist?
67
- if same_content?(source, dest)
68
- return { name: name, action: :unchanged, path: dest }
69
- end
54
+ def validate_provider!(provider)
55
+ return if PROVIDERS.include?(provider.to_s)
70
56
 
71
- unless force
72
- return { name: name, action: :conflict, path: dest } if dry_run
57
+ raise Space::Core::Error, "Unknown provider '#{provider}'. Expected one of: #{PROVIDERS.join(', ')}"
58
+ end
73
59
 
74
- raise Error,
75
- "Refusing to overwrite existing skill at #{dest}. Re-run with --force."
76
- end
60
+ def pi_agent_dir(env:)
61
+ Pathname.new(env.fetch("PI_CODING_AGENT_DIR", File.join(Space::Core::XDG.home(env: env), ".pi", "agent")))
62
+ end
77
63
 
78
- unless dry_run
79
- FileUtils.rm_rf(dest)
80
- FileUtils.cp_r(source, dest)
81
- end
82
- { name: name, action: dry_run ? :would_update : :updated, path: dest }
83
- else
84
- unless dry_run
85
- FileUtils.mkdir_p(dest.parent)
86
- FileUtils.cp_r(source, dest)
64
+ def install_skill(source, dest, force:, dry_run:)
65
+ name = source.basename.to_s
66
+
67
+ if dest.exist?
68
+ if same_content?(source, dest)
69
+ return { name: name, action: :unchanged, path: dest }
70
+ end
71
+
72
+ unless force
73
+ return { name: name, action: :conflict, path: dest } if dry_run
74
+
75
+ raise Space::Core::Error,
76
+ "Refusing to overwrite existing skill at #{dest}. Re-run with --force."
77
+ end
78
+
79
+ unless dry_run
80
+ FileUtils.rm_rf(dest)
81
+ FileUtils.cp_r(source, dest)
82
+ end
83
+ { name: name, action: dry_run ? :would_update : :updated, path: dest }
84
+ else
85
+ unless dry_run
86
+ FileUtils.mkdir_p(dest.parent)
87
+ FileUtils.cp_r(source, dest)
88
+ end
89
+ { name: name, action: dry_run ? :would_install : :installed, path: dest }
87
90
  end
88
- { name: name, action: dry_run ? :would_install : :installed, path: dest }
89
91
  end
90
- end
91
92
 
92
- def same_content?(source, dest)
93
- return false unless dest.directory?
93
+ def same_content?(source, dest)
94
+ return false unless dest.directory?
94
95
 
95
- source_files = Dir.glob("#{source}/**/*").reject { |f| File.directory?(f) }
96
- dest_files = Dir.glob("#{dest}/**/*").reject { |f| File.directory?(f) }
96
+ source_files = Dir.glob("#{source}/**/*").reject { |f| File.directory?(f) }
97
+ dest_files = Dir.glob("#{dest}/**/*").reject { |f| File.directory?(f) }
97
98
 
98
- return false if source_files.length != dest_files.length
99
+ return false if source_files.length != dest_files.length
99
100
 
100
- source_files.sort.zip(dest_files.sort).all? do |sf, df|
101
- rel = sf.sub("#{source}/", "")
102
- df.end_with?(rel) && File.read(sf) == File.read(df)
101
+ source_files.sort.zip(dest_files.sort).all? do |sf, df|
102
+ rel = sf.sub("#{source}/", "")
103
+ df.end_with?(rel) && File.read(sf) == File.read(df)
104
+ end
103
105
  end
104
106
  end
105
107
  end
@@ -2,11 +2,11 @@
2
2
 
3
3
  > Cross-iteration table of contents for the Architect Loop. Per-iteration detail lives in
4
4
  > architecture/I<NN>-<iteration>.md — this file only indexes the iterations and carries
5
- > mission-wide state. Keep it short (~150 lines): the next session must grok it
5
+ > project-wide state. Keep it short (~150 lines): the next session must grok it
6
6
  > in under a minute. Not in the committed architecture = didn't happen.
7
7
  >
8
- > Durable mission contract: `architecture/BRIEF.md` (numbered §sections, cited as BRIEF §N).
9
- > Create it with `architect brief new`. Edits to a §section are mission-scope decisions —
8
+ > Durable project contract: `architecture/BRIEF.md` (numbered §sections, cited as BRIEF §N).
9
+ > Create it with `architect brief new`. Edits to a §section are project-scope decisions —
10
10
  > log them in the Decisions log below, never as silent per-iteration drift.
11
11
 
12
12
  ## TL;DR (keep current)
@@ -34,13 +34,24 @@
34
34
 
35
35
  ## Iteration index
36
36
 
37
- <!-- Add a row per iteration. Create iterations with `architect new <name>`. -->
37
+ <!-- Rows are added here only for SCAFFOLDED iterations. `architect new <name>` allocates
38
+ the ordinal at spec-time and scaffolds architecture/I<NN>-<name>.md. Do NOT
39
+ pre-number planned work — un-numbered planned items live in the Backlog below. -->
38
40
 
39
41
  | I# | Iteration | Status | freeze_sha | Integration branch | Verdict | File |
40
42
  |----|-----------|--------|-----------|--------------------|---------|------|
41
43
 
42
44
  Status values: speccing → frozen → dispatched → in-flight → awaiting-verdict → done.
43
45
 
46
+ ## Backlog (ordered; no ordinal until `architect new`)
47
+
48
+ <!-- Next-up first. Each item gets its ordinal only when it is about to be specced:
49
+ run `architect new <name>` at that point to allocate the ordinal at spec-time and
50
+ scaffold the iteration file. Pre-numbering planned work forces renumber churn every
51
+ time priorities reshuffle. -->
52
+
53
+ - _[next planned item]_
54
+
44
55
  ## Open items for the human / architect
45
56
 
46
57
  <!-- Blocking items: unresolved disagreements (which iteration), scope questions,
@@ -1,11 +1,11 @@
1
1
  # BRIEF — <%= @_title %>
2
2
 
3
- > Durable mission contract for the Architect Loop. The numbered §sections below are the
3
+ > Durable project contract for the Architect Loop. The numbered §sections below are the
4
4
  > stable, cross-iteration address space: every iteration cites them as **BRIEF §N** in its
5
5
  > Grounds, Specification, Acceptance Criteria, and Verdict (e.g. `(BRIEF §3.1)`), the way each
6
- > gate addresses its intent back to one frozen reference. Frozen early; edits are mission-scope
6
+ > gate addresses its intent back to one frozen reference. Frozen early; edits are project-scope
7
7
  > decisions logged in ARCHITECT.md's Decisions log, never silent per-iteration drift. Optional —
8
- > a discovery mission may defer this and cite per-iteration Grounds until the shape stabilizes,
8
+ > a discovery project may defer this and cite per-iteration Grounds until the shape stabilizes,
9
9
  > then promote the consolidated picture here once.
10
10
 
11
11
  ## 1. Goal & non-goals
@@ -38,6 +38,6 @@ frozen proof. An iteration's Specification cites its row here (e.g. "BRIEF §5 S
38
38
  <!-- Known risks and the things a builder's PHASE 0 should challenge — cited from specs as
39
39
  "BRIEF §6". -->
40
40
 
41
- ## 7. Definition of done (whole mission)
41
+ ## 7. Definition of done (whole project)
42
42
 
43
- <!-- The mission-level DoD. Iteration gates diff against "BRIEF §7 DoD". -->
43
+ <!-- The project-level DoD. Iteration gates diff against "BRIEF §7 DoD". -->
@@ -32,16 +32,27 @@ Write + commit: `architect section <%= @_name %> specification --from <file>`. -
32
32
 
33
33
  ## Acceptance Criteria
34
34
 
35
- <!-- PROOF. Exact gate commands + thresholds. `architect freeze <%= @_name %>`
36
- commits this file and records its SHA as freeze_sha. Read-only afterward — any
37
- change to Grounds/Specification/Acceptance Criteria = automatic iteration FAIL. -->
35
+ <!-- PROOF. Write the prose conditions of correctness (AC1, AC2, …) that the
36
+ architect judges against. Runnable checks live in the fenced ```gates block below
37
+ (parsed at freeze time absent or empty is allowed; malformed fails freeze).
38
+ `architect freeze <%= @_name %>` commits this file and records its SHA as
39
+ freeze_sha. Read-only afterward — any change to Grounds/Specification/Acceptance
40
+ Criteria = automatic iteration FAIL. -->
38
41
 
39
42
  > Gate-pass is necessary, not sufficient: the architect also reads the diff against the
40
43
  > cited **BRIEF §sections** intent and the §1 cardinal invariant before the verdict.
41
44
 
42
- | AC# | Command | Threshold | Brief § |
43
- |-----|---------|-----------|---------|
44
- | | | | |
45
+ **AC1.** ...
46
+
47
+ ```gates
48
+ # Each gate backs one prose AC above. Remove the comment markers to activate.
49
+ # - id: suite-green # unique slug within the iteration (required)
50
+ # ac: AC1 # which prose AC this gate backs (required)
51
+ # cwd: repos/my-repo # run dir, relative to space root (optional; consumed by I03)
52
+ # cmd: bundle exec rake test # shell command (required, non-empty)
53
+ # expect: # at least one of: exit_code, stdout_match, threshold
54
+ # exit_code: 0
55
+ ```
45
56
 
46
57
  ## Builder Prompt
47
58
 
@@ -1,27 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- $LOAD_PATH.unshift File.expand_path("../vendor/repo-tender/lib", __dir__)
4
- require "space_architect/pristine"
3
+ require "space_core"
4
+ require "space_src"
5
5
 
6
- require_relative "space_architect/version"
7
- require_relative "space_architect/errors"
8
- require_relative "space_architect/warnings"
9
- SpaceArchitect::Warnings.disable_experimental!
10
- require_relative "space_architect/atomic_write"
11
- require_relative "space_architect/xdg"
12
- require_relative "space_architect/config"
13
- require_relative "space_architect/state"
14
- require_relative "space_architect/slugger"
15
- require_relative "space_architect/space"
16
- require_relative "space_architect/repo_reference"
17
- require_relative "space_architect/repo_resolver"
18
- require_relative "space_architect/git_client"
19
- require_relative "space_architect/mise_client"
20
- require_relative "space_architect/space_store"
21
- require_relative "space_architect/shell_integration"
22
- require_relative "space_architect/skill_installer"
23
- require_relative "space_architect/terminal"
24
6
  require_relative "space_architect/harness"
7
+ require_relative "space_architect/run_creator"
25
8
  require_relative "space_architect/dispatcher"
26
- require_relative "space_architect/architect_mission"
9
+ require_relative "space_architect/gate_lint"
10
+ require_relative "space_architect/gate_evaluator"
11
+ require_relative "space_architect/architect_project"
12
+ require_relative "space_architect/skill_installer"
13
+ require_relative "space_architect/research"
27
14
  require_relative "space_architect/cli"
@@ -2,7 +2,7 @@
2
2
 
3
3
  require "fileutils"
4
4
 
5
- module SpaceArchitect
5
+ module Space::Core
6
6
  module AtomicWrite
7
7
  module_function
8
8
 
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/cli"
4
+
5
+ module Space::Core::CLI
6
+ # Base for every space/architect command. dry-cli (>= 0.7.0) copies a
7
+ # superclass's options to its subclasses, so the global colour options are
8
+ # declared once here and inherited everywhere instead of being mixed in per
9
+ # command. Helpers (terminal/store/render) ride along by inheritance too.
10
+ #
11
+ # The `src` binary has its own output-mode system (--plain/--json) and does
12
+ # NOT inherit from this base.
13
+ class BaseCommand < Dry::CLI::Command
14
+ include Helpers
15
+
16
+ option :color, type: :string, default: "auto", desc: "Color output: auto, always, never"
17
+ option :colors, type: :string, desc: "Alias for --color"
18
+ end
19
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../oci_packer"
4
+ require_relative "../oci_builder"
5
+
6
+ module Space::Core::CLI
7
+ class Build < BaseCommand
8
+ desc "Build (and tag) the OCI image for the current space"
9
+
10
+ def call(**opts)
11
+ setup_terminal(**opts.slice(:color, :colors))
12
+ handle_errors do
13
+ result = store.current.bind do |space|
14
+ out_dir = space.path.join("build", "oci")
15
+ Space::Core::OciPacker.new(space: space, output_dir: out_dir).generate.bind do
16
+ Space::Core::OciBuilder.new(space: space, output_dir: out_dir).command
17
+ end
18
+ end
19
+ render(result) do |argv|
20
+ terminal.say "Building: #{argv.join(' ')}"
21
+ out.flush
22
+ Kernel.exec(*argv)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Space::Core::CLI
4
+ module Config
5
+ class Show < BaseCommand
6
+ desc "Show current config"
7
+
8
+ def call(**opts)
9
+ setup_terminal(**opts.slice(:color, :colors))
10
+ handle_errors do
11
+ rows = Space::Core::Config::EDITABLE_KEYS.map do |key|
12
+ value = project_config.data[key]
13
+ [key, value.nil? ? "" : value.to_s]
14
+ end
15
+ terminal.say terminal.table(%w[Key Value], rows)
16
+ CLI.record_outcome(Outcome.new(exit_code: 0))
17
+ end
18
+ end
19
+ end
20
+
21
+ class ConfigPath < BaseCommand
22
+ desc "Print the config file path"
23
+
24
+ def call(**opts)
25
+ setup_terminal(**opts.slice(:color, :colors))
26
+ handle_errors do
27
+ terminal.say terminal.path(project_config.path)
28
+ CLI.record_outcome(Outcome.new(exit_code: 0))
29
+ end
30
+ end
31
+ end
32
+
33
+ class Set < BaseCommand
34
+ desc "Set a config key"
35
+ argument :key, required: true, desc: "Config key"
36
+ argument :value, required: true, desc: "Config value"
37
+
38
+ def call(key:, value:, **opts)
39
+ setup_terminal(**opts.slice(:color, :colors))
40
+ handle_errors do
41
+ project_config.set(key, value)
42
+ stored = project_config.data[key]
43
+ terminal.success "Set #{key}=#{stored.nil? ? '' : stored.to_s}"
44
+ CLI.record_outcome(Outcome.new(exit_code: 0))
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Space::Core::CLI
4
+ class Current < BaseCommand
5
+ desc "Show the current space"
6
+
7
+ def call(**opts)
8
+ setup_terminal(**opts.slice(:color, :colors))
9
+ render(store.find) do |space|
10
+ terminal.say space.id.to_s
11
+ terminal.say terminal.path(space.path)
12
+ CLI.record_outcome(Outcome.new(exit_code: 0))
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/cli"
4
+ require "pastel"
5
+
6
+ module Space::Core::CLI
7
+ # Colourful replacement for dry-cli's plain `Usage` listing — the "global
8
+ # help" shown by `space`, `architect`, and every bare namespace (`space repo`,
9
+ # `worktree`, ...). Per-command help still flows through dry-cli's Banner,
10
+ # whose content we like; only the listing is reskinned.
11
+ #
12
+ # We reopen Dry::CLI::Usage.call (below) to delegate here, so BOTH the
13
+ # intercepted top-level help and dry-cli's own bare-namespace path get the
14
+ # same treatment from one place. Colour follows CLI.help_pastel, set once per
15
+ # invocation from the output stream's tty-ness and --color, so piped and test
16
+ # output stay plain. The `src` binary never loads space_core, so its own plain
17
+ # Usage is untouched.
18
+ module Help
19
+ # One tagline per binary — the root header is served for both `space` and
20
+ # `architect` from here, so we pick by $PROGRAM_NAME. The default covers the
21
+ # test runner and any other invocation where the binary can't be identified.
22
+ TAGLINES = {
23
+ "space" => "date-prefixed workspaces; repos provisioned on fibers at copy-on-write speed",
24
+ "architect" => "the Architect Loop: structured judgment plus a fleet of headless AI builders"
25
+ }.freeze
26
+ DEFAULT_TAGLINE = TAGLINES.fetch("space")
27
+
28
+ module_function
29
+
30
+ def call(result, pastel: CLI.help_pastel)
31
+ rows = listing(result)
32
+ width = rows.map { |label, _| label.length }.max || 0
33
+
34
+ lines = rows.map do |label, description|
35
+ painted = pastel.cyan(label.ljust(width))
36
+ description ? " #{painted} #{pastel.dim("# #{description}")}" : " #{painted}"
37
+ end
38
+
39
+ [header(result, pastel), pastel.bold("Commands:"), *lines, footer(result, pastel)]
40
+ .compact.join("\n")
41
+ end
42
+
43
+ # The richer header only makes sense at the true root (`space` / `architect`),
44
+ # not on every sub-namespace listing.
45
+ def header(result, pastel)
46
+ return unless result.names.empty?
47
+
48
+ "#{pastel.bold.cyan("space-architect")} #{pastel.dim(Space::Core::VERSION)} " \
49
+ "#{pastel.dim("— #{tagline}")}\n"
50
+ end
51
+
52
+ # The tagline for the binary in hand: `space` and `architect` each get their
53
+ # own, keyed off $PROGRAM_NAME (the same signal program_prefix reads).
54
+ def tagline
55
+ TAGLINES.fetch(File.basename($PROGRAM_NAME), DEFAULT_TAGLINE)
56
+ end
57
+
58
+ def footer(result, pastel)
59
+ "\n#{pastel.dim("Run `#{program_prefix(result)} <command> --help` for details on a command.")}"
60
+ end
61
+
62
+ # "space" at the root, "space repo" inside a namespace. The `space`/`architect`
63
+ # binaries inject their name into ARGV, so $PROGRAM_NAME and the leading
64
+ # namespace segment can collide ("space space ..."); drop the duplicate.
65
+ def program_prefix(result)
66
+ prog = File.basename($PROGRAM_NAME)
67
+ names = result.names.dup
68
+ names.shift if names.first == prog
69
+ [prog, *names].join(" ")
70
+ end
71
+
72
+ # [[label_with_banner, description_or_nil], ...] sorted by command name.
73
+ def listing(result)
74
+ result.children.sort_by { |name, _| name }.filter_map do |name, node|
75
+ next if node.hidden
76
+
77
+ [label(result, name, node), description(node)]
78
+ end
79
+ end
80
+
81
+ def label(result, name, node)
82
+ "#{program_prefix(result)} #{name}#{banner(node)}"
83
+ end
84
+
85
+ def banner(node)
86
+ if node.command && node.leaf? && node.children?
87
+ " [ARGUMENT|SUBCOMMAND]"
88
+ elsif node.leaf?
89
+ arguments(node.command)
90
+ else
91
+ " [SUBCOMMAND]"
92
+ end
93
+ end
94
+
95
+ def arguments(command)
96
+ return "" unless command.respond_to?(:required_arguments)
97
+
98
+ names = command.required_arguments.map { |arg| arg.name.to_s.upcase }
99
+ names += command.optional_arguments.map { |arg| "[#{arg.name.to_s.upcase}]" }
100
+ names.empty? ? "" : " #{names.join(" ")}"
101
+ end
102
+
103
+ def description(node)
104
+ return unless node.leaf? && node.command.respond_to?(:description)
105
+
106
+ node.command.description
107
+ end
108
+ end
109
+ end
110
+
111
+ # Route dry-cli's plain namespace/root listing through our colourful renderer.
112
+ # We replace Usage.call wholesale and depend only on the LookupResult/Node API
113
+ # (children, command, leaf?/children?/hidden, names) rather than copying Usage's
114
+ # internals — see notes/ruby-cli-gems-report.md.
115
+ module Dry
116
+ class CLI
117
+ module Usage
118
+ def self.call(result)
119
+ Space::Core::CLI::Help.call(result)
120
+ end
121
+ end
122
+ end
123
+ end