space-architect 9.4.0 → 9.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e768903f0762630e4c4c510a83621cf4c82be30ea7d95026d794f90d70205f44
4
- data.tar.gz: 77c7b1faae07696200558929456ab481aa5f8f061f69a0ff923fac53a4957f2c
3
+ metadata.gz: 4de640b748008dc4484903c88d496366528cb9eb294002abad5bc02547816198
4
+ data.tar.gz: 4aa87deb60eee6dc8d768d6f333204137d7ddb49a3e94640263fce5711623e3e
5
5
  SHA512:
6
- metadata.gz: 27e9c5abd1ae5a60b42aee5bed88640b3dd49352214b2af420384333b8d93c558369e14220f4f996729d1ccdcf4155552e05dbd59c0ffaf05ca81fe0ea4f9489
7
- data.tar.gz: 5e86aa794e2ec154f8e797040cdadd6a59c43489bf98ffdb070f8e7d90a82572e73f19ba5cadedfebdd90fea29062fd0fa873e9bba08cf06f1277483a60e7ee0
6
+ metadata.gz: 5e383f34c36693c6ad9290ec9db84815835545676c194d4c06d79d630c6bc4f98ba6b1c7072570c60b4ce3df2b782d0a6e064b0d1ed5d31fdd9baacc6dbe0342
7
+ data.tar.gz: b957f8f1a303a9a2c88ad0c569a8ce8b572740dfffb1da6aa203b0e66fb15f1984d711479549c6d1a35ae7ece15a502f3f48994f5614bf09a06346b3392b129a
data/CHANGELOG.md CHANGED
@@ -5,6 +5,31 @@ 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
+ ## [9.5.0] - 2026-10-17
9
+
10
+ ### Added
11
+
12
+ - **`architect install`** — the one-shot machine install: skills
13
+ (`--provider`, default pi), the architect-loop extension, then the role
14
+ playbooks, in that order (install-agents renders from the extension the
15
+ prior step installs). Each step aborts loudly; no partial silent states.
16
+ - **`architect install-agents`** — the role playbooks to
17
+ `~/.pi/agent/agents/`, rendered by the installed architect-loop extension
18
+ (the single owner of the playbook text) with global-routing pins;
19
+ write-if-changed, `--force`, `--dry-run` like install-skills. Loud
20
+ failures name the prerequisite (`install-extensions`) or the cure
21
+ (`routing init --global`).
22
+
23
+ ### Changed
24
+
25
+ - **install-skills targets pi by default** (was claude; `--provider` still
26
+ selects claude/codex/opencode).
27
+ - **looplog tags**: the narrator abbreviates to `narr` (it previously
28
+ truncated to "architect-"), and lane-worktree tags carry the iteration
29
+ ordinal plus the lane, resolved against the space's iteration ids —
30
+ `[bld:67-core]` instead of the old clipped
31
+ `[bld:rig-oracle-repin-core]`.
32
+
8
33
  ## [9.4.0] - 2026-10-17
9
34
 
10
35
  ### Added
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+
5
+ module Space
6
+ module Architect
7
+ # Installs the role playbooks (architect-coordinator/spec/builder/judge/
8
+ # narrator) to ~/.pi/agent/agents. The playbook text is rendered by the
9
+ # installed architect-loop extension itself — the extension owns that text
10
+ # (it renders the per-space .pi/agents files from the same source), so this
11
+ # installer carries no second copy to drift. Routing pins come from the
12
+ # global architect.yaml (ENV["SPACE_ARCHITECT_GLOBAL_CONFIG"] overrides the
13
+ # path, honored inside the extension): the user-level files are the
14
+ # machine-wide fallback for sessions that cannot see a space's .pi dir;
15
+ # a space's own .pi/agents files always win inside that space.
16
+ module AgentInstaller
17
+ ROLES = %w[coordinator spec builder judge narrator].freeze
18
+
19
+ # Renders each role's agent file and prints it between ===FILE===<role>
20
+ # markers. Dynamic import (node -e runs CJS; import() is legal there) so
21
+ # the .ts source — type-stripped by node — loads with its own package's
22
+ # node_modules. The scratch startDir forces global-only routing.
23
+ RENDER_SCRIPT = <<~JS
24
+ const { pathToFileURL } = require("node:url");
25
+ const fs = require("node:fs"), os = require("node:os"), path = require("node:path");
26
+ import(pathToFileURL(process.argv[1]).href).then(({ renderAgentFile, resolveConfig }) => {
27
+ const config = resolveConfig(fs.mkdtempSync(path.join(os.tmpdir(), "architect-agents-")), {});
28
+ if (!config) {
29
+ console.error("no architect.yaml found — run `architect routing init --global` to scaffold the machine-wide config first");
30
+ process.exit(1);
31
+ }
32
+ for (const role of ["coordinator", "spec", "builder", "judge", "narrator"]) {
33
+ console.log("===FILE===" + role);
34
+ console.log(renderAgentFile(role, config));
35
+ }
36
+ }).catch((error) => {
37
+ console.error(error && error.message ? error.message : String(error));
38
+ process.exit(1);
39
+ });
40
+ JS
41
+
42
+ class << self
43
+ def install(env: ENV, force: false, dry_run: false, dest: nil, extension_dir: nil)
44
+ source = extension_dir ? Pathname.new(extension_dir).join("architect-loop.ts") : locate_extension(env)
45
+ unless source&.exist?
46
+ raise Space::Core::Error,
47
+ "architect-loop.ts not found under #{search_root(env)} — run `architect install-extensions` first " \
48
+ "(install-agents renders its playbooks from the installed extension)"
49
+ end
50
+
51
+ dest ||= default_dest(env)
52
+ rendered = render_via_node(source, env)
53
+ results = ROLES.map do |role|
54
+ write_agent(dest.join("architect-#{role}.md"), rendered.fetch(role), force: force, dry_run: dry_run)
55
+ end
56
+ { dest_root: dest, agents: results, dry_run: dry_run }
57
+ end
58
+
59
+ private
60
+
61
+ def default_dest(env)
62
+ Pathname.new(env.fetch("PI_AGENTS_DIR", File.join(pi_agent_dir(env), "agents")))
63
+ end
64
+
65
+ # Same convention as SkillInstaller: PI_CODING_AGENT_DIR or ~/.pi/agent.
66
+ def pi_agent_dir(env)
67
+ Pathname.new(env.fetch("PI_CODING_AGENT_DIR", File.join(Space::Core::XDG.home(env: env), ".pi", "agent")))
68
+ end
69
+
70
+ def search_root(env)
71
+ File.join(pi_agent_dir(env), "git", "**")
72
+ end
73
+
74
+ # The installed extension package: pi clones it under
75
+ # <agent dir>/git/<host>/<owner>/pi-extensions/. First match wins;
76
+ # --extension-dir (tests) bypasses discovery.
77
+ def locate_extension(env)
78
+ Dir.glob(File.join(search_root(env), "pi-extensions", "architect-loop.ts")).first # paths:exempt — enumerates pi's own managed install layout (git/<host>/<owner>/pi-extensions), matching exactly the declared package, never user project files
79
+ &.then { |p| Pathname.new(p) }
80
+ end
81
+
82
+ def render_via_node(source, env)
83
+ out, err, status = Open3.capture3(env, "node", "-e", RENDER_SCRIPT, source.to_s)
84
+ unless status.success?
85
+ raise Space::Core::Error, "rendering playbooks via node failed (exit #{status.exitstatus}):\n#{out + err}"
86
+ end
87
+
88
+ rendered = {}
89
+ marker = nil
90
+ out.each_line do |line|
91
+ if (m = line.match(/\A===FILE===(\w+)\n?\z/))
92
+ marker = m[1]
93
+ rendered[marker] = +""
94
+ elsif marker
95
+ rendered[marker] << line
96
+ end
97
+ end
98
+ missing = ROLES - rendered.keys
99
+ raise Space::Core::Error, "rendered output missing roles: #{missing.join(', ')}" if missing.any?
100
+ rendered
101
+ rescue Errno::ENOENT
102
+ raise Space::Core::Error,
103
+ "node: command not found — install-agents renders playbooks through the installed " \
104
+ "architect-loop extension, which needs node on PATH (it ships with pi)."
105
+ end
106
+
107
+ def write_agent(dest, content, force:, dry_run:)
108
+ name = dest.basename.to_s
109
+ if dest.exist?
110
+ return { name: name, action: :unchanged, path: dest } if File.read(dest) == content
111
+ return { name: name, action: :conflict, path: dest } if dry_run
112
+
113
+ raise Space::Core::Error,
114
+ "Refusing to overwrite existing playbook at #{dest}. Re-run with --force." unless force
115
+
116
+ File.write(dest, content)
117
+ { name: name, action: :updated, path: dest }
118
+ else
119
+ FileUtils.mkdir_p(dest.parent)
120
+ File.write(dest, content) unless dry_run
121
+ { name: name, action: dry_run ? :would_install : :installed, path: dest }
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end
@@ -6,6 +6,7 @@ require_relative "../loop_cost"
6
6
  require_relative "../runlog"
7
7
  require_relative "../looplog"
8
8
  require_relative "../extension_installer"
9
+ require_relative "../agent_installer"
9
10
 
10
11
  module Space::Architect
11
12
  module CLI
@@ -1064,12 +1065,12 @@ module Space::Architect
1064
1065
  class InstallSkills < BaseCommand
1065
1066
  desc "Install bundled skills (architect, architect-discuss, architect-action, architect-status, architect-research) for a harness"
1066
1067
  phase 53, "Project"
1067
- option :provider, default: "claude", desc: "Skill install target (default: claude; validated by the installer)"
1068
+ option :provider, default: "pi", desc: "Skill install target (default: pi; validated by the installer)"
1068
1069
  option :project, type: :boolean, default: false, desc: "Install to CWD instead of global"
1069
1070
  option :force, type: :boolean, default: false, desc: "Overwrite existing skills that differ"
1070
1071
  option :dry_run, type: :boolean, default: false, desc: "Print what would happen without writing files"
1071
1072
 
1072
- def call(provider: "claude", project: false, force: false, dry_run: false, **opts)
1073
+ def call(provider: "pi", project: false, force: false, dry_run: false, **opts)
1073
1074
  setup_terminal(**opts.slice(:color, :colors))
1074
1075
  handle_errors do
1075
1076
  result = SkillInstaller.install(provider, project: project, force: force,
@@ -1102,6 +1103,61 @@ module Space::Architect
1102
1103
  end
1103
1104
  end
1104
1105
 
1106
+ # Role playbooks to ~/.pi/agent/agents, rendered from the installed
1107
+ # architect-loop extension (the single owner of the playbook text) with
1108
+ # global-routing pins — the machine-wide fallback for sessions that
1109
+ # cannot see a space's .pi dir. Needs install-extensions to have run.
1110
+ class InstallAgents < BaseCommand
1111
+ desc "Install the role playbooks (architect-coordinator/spec/builder/judge/narrator) to ~/.pi/agent/agents, rendered from the installed architect-loop extension"
1112
+ phase 53, "Project"
1113
+ option :force, type: :boolean, default: false, desc: "Overwrite existing playbooks that differ"
1114
+ option :dry_run, type: :boolean, default: false, desc: "Print what would happen without writing files"
1115
+
1116
+ def call(force: false, dry_run: false, **opts)
1117
+ setup_terminal(**opts.slice(:color, :colors))
1118
+ handle_errors do
1119
+ result = AgentInstaller.install(env: project_config.env, force: force, dry_run: dry_run)
1120
+ verb = dry_run ? "Would install" : "Installed"
1121
+ terminal.say "#{verb} role playbooks for pi → #{terminal.path(result[:dest_root])}"
1122
+ result[:agents].each do |a|
1123
+ terminal.say " #{a[:name]}: #{terminal.style_skill_action(a[:action])} (#{terminal.path(a[:path])})"
1124
+ end
1125
+ CLI.record_outcome(Outcome.new(exit_code: 0))
1126
+ end
1127
+ end
1128
+ end
1129
+
1130
+ # The one-shot machine install: skills (pi by default), the
1131
+ # architect-loop extension, then the role playbooks — in that order,
1132
+ # because install-agents renders from the extension the prior step just
1133
+ # installed. Each step aborts loudly on failure; there are no partial
1134
+ # silent states to reason about afterwards.
1135
+ class Install < BaseCommand
1136
+ desc "Install everything for this machine: skills (--provider, default pi), the architect-loop extension, then the role playbooks"
1137
+ phase 53, "Project"
1138
+ option :provider, default: "pi", desc: "Skill install target (default: pi; validated by the installer; passed through to install-skills)"
1139
+ option :force, type: :boolean, default: false, desc: "Overwrite existing skills/playbooks that differ (passed through)"
1140
+
1141
+ def call(provider: "pi", force: false, **opts)
1142
+ setup_terminal(**opts.slice(:color, :colors))
1143
+ handle_errors do
1144
+ skills = SkillInstaller.install(provider, project: false, force: force,
1145
+ env: project_config.env)
1146
+ terminal.say "skills → #{terminal.path(skills[:dest_root])}"
1147
+ skills[:skills].each { |s| terminal.say " #{s[:name]}: #{terminal.style_skill_action(s[:action])}" }
1148
+
1149
+ extensions = ExtensionInstaller.install(env: project_config.env)
1150
+ extensions[:steps].each { |s| terminal.say " #{s[:cmd]} (exit #{s[:exit_code]})" }
1151
+
1152
+ agents = AgentInstaller.install(env: project_config.env, force: force)
1153
+ terminal.say "role playbooks → #{terminal.path(agents[:dest_root])}"
1154
+ agents[:agents].each { |a| terminal.say " #{a[:name]}: #{terminal.style_skill_action(a[:action])}" }
1155
+
1156
+ CLI.record_outcome(Outcome.new(exit_code: 0))
1157
+ end
1158
+ end
1159
+ end
1160
+
1105
1161
  module Routing
1106
1162
  # I09: `architect routing init` — the self-service cure for the
1107
1163
  # missing-config wall (default_model_inform's hint names this command).
@@ -1378,8 +1434,10 @@ Space::Architect::CLI::Registry.register "evidence", Space::Architect::CLI::Arc
1378
1434
  Space::Architect::CLI::Registry.register "merge", Space::Architect::CLI::Architect::Merge
1379
1435
  Space::Architect::CLI::Registry.register "integrate", Space::Architect::CLI::Architect::Integrate
1380
1436
  Space::Architect::CLI::Registry.register "gate", Space::Architect::CLI::Architect::Gate
1437
+ Space::Architect::CLI::Registry.register "install", Space::Architect::CLI::Architect::Install
1381
1438
  Space::Architect::CLI::Registry.register "install-skills", Space::Architect::CLI::Architect::InstallSkills
1382
1439
  Space::Architect::CLI::Registry.register "install-extensions", Space::Architect::CLI::Architect::InstallExtensions
1440
+ Space::Architect::CLI::Registry.register "install-agents", Space::Architect::CLI::Architect::InstallAgents
1383
1441
  Space::Architect::CLI::Registry.register "runlog", Space::Architect::CLI::Architect::Runlog
1384
1442
  Space::Architect::CLI::Registry.register "looplog", Space::Architect::CLI::Architect::Looplog
1385
1443
  Space::Architect::CLI::Registry.register "bug-report", Space::Architect::CLI::Architect::BugReport
@@ -56,7 +56,8 @@ module Space
56
56
  "architect-coordinator" => "coord",
57
57
  "architect-spec" => "spec",
58
58
  "architect-judge" => "judge",
59
- "architect-builder" => "bld"
59
+ "architect-builder" => "bld",
60
+ "architect-narrator" => "narr"
60
61
  }.freeze
61
62
 
62
63
  TAG_COLORS = %w[36 35 33 32 34 95 96 91].freeze
@@ -166,13 +167,23 @@ module Space
166
167
  [cwd, task]
167
168
  end
168
169
 
169
- # [bld:I35-editor-multiline-core] when cwd is a lane worktree, else the role.
170
- def tag_label(agent, cwd)
170
+ # [bld:67-core] when cwd is a lane worktree — the iteration ordinal
171
+ # plus the lane name, resolved against the space's known iteration ids
172
+ # (architecture/I<NN>-<name>.md) so a hyphenated iteration slug cannot
173
+ # bleed into the lane (I06-editor-markdown-markdown-seam → 06-markdown-seam).
174
+ # An unrecognized dir (no architecture/, or a legacy name) falls back
175
+ # to the whole dir name minus the I<NN>- prefix, clipped.
176
+ def tag_label(agent, cwd, iteration_ids: [])
171
177
  role = ROLE_ABBREV.fetch(agent, agent[0, 10])
172
178
  return role unless cwd && File.basename(cwd) == "wt"
173
179
 
174
- lane = File.basename(File.dirname(cwd)).sub(/\A[Ii]\d+-/, "")
175
- "#{role}:#{lane[0, 26]}"
180
+ dir = File.basename(File.dirname(cwd))
181
+ ordinal = dir[/\A[Ii](\d+)-/, 1]
182
+ return "#{role}:#{dir[0, 26]}" unless ordinal
183
+
184
+ lane = iteration_ids.filter_map { |id| dir.delete_prefix("#{id}-") if dir.start_with?("#{id}-") }.first
185
+ lane ||= dir.sub(/\A[Ii]\d+-/, "")
186
+ "#{role}:#{ordinal}-#{lane[0, 26]}"
176
187
  end
177
188
 
178
189
  def paint_tag(label, run_id)
@@ -350,7 +361,7 @@ module Space
350
361
  cwd, task = backscan(t.path)
351
362
  @io.puts format("%s %-9s %5s %5.0fm %7sK %s %s",
352
363
  active ? green("●") : dim("·"),
353
- tag_label(t.agent, cwd),
364
+ tag_label(t.agent, cwd, iteration_ids: iteration_ids),
354
365
  dim(Time.at(t.mtime).strftime("%H:%M")),
355
366
  age_min, t.size / 1024,
356
367
  dim(t.run_id[0, 8]),
@@ -390,9 +401,18 @@ module Space
390
401
 
391
402
  private
392
403
 
404
+ # The space's iteration ids (architecture/I<NN>-<name>.md basenames) —
405
+ # the authority for splitting a lane dir name into iteration and lane
406
+ # without guessing at hyphen boundaries.
407
+ def iteration_ids
408
+ # paths:exempt — the I[0-9]*-*.md pattern structurally cannot match a dotfile-prefixed name; one non-hidden directory level, no dotfile hazard
409
+ @iteration_ids ||= Dir.glob(File.join(@space.to_s, "architecture", "I[0-9]*-*.md"))
410
+ .map { |p| File.basename(p, ".md") }
411
+ end
412
+
393
413
  def adopt(t)
394
414
  cwd, task = backscan(t.path)
395
- tag = paint_tag(tag_label(t.agent, cwd), t.run_id)
415
+ tag = paint_tag(tag_label(t.agent, cwd, iteration_ids: iteration_ids), t.run_id)
396
416
  run = Run.new(t.path, t.run_id, t.agent, tag,
397
417
  Renderer.new(limit: @limit, think_limit: @env["RUNLOG_THINK"]&.to_i, color: @color),
398
418
  @replay ? 0 : t.size, +"", stamp_now)
@@ -4,6 +4,11 @@
4
4
  // and commands bash cannot parse (a parse error at tool time burns a turn).
5
5
  // Parse-level only: `bash -n` cannot catch runtime failures — those are the
6
6
  // bash tool's own domain.
7
+ //
8
+ // NOTE: a sibling copy of the guard logic lives in jetpks/pi-extensions
9
+ // (imported by architect-loop.ts, which applies the same denials to sessions
10
+ // whose cwd is a lane worktree — coordinator-spawned builders cannot get -e
11
+ // injection). The logic is kept identical across both — change them together.
7
12
  import { spawnSync } from "node:child_process";
8
13
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
9
14
 
@@ -80,6 +85,13 @@ function parseDenial(command: string): string | null {
80
85
  `fix the command's syntax — bash said: ${(check.stderr ?? "").trim()}`;
81
86
  }
82
87
 
88
+ // The combined denial for a bash command: why it must not run, or null.
89
+ // Exported pure so architect-loop.ts can apply the same rules to sessions
90
+ // running inside lane worktrees (see the NOTE above).
91
+ export function guardDenial(command: string): string | null {
92
+ return gitWriteDenial(command) ?? parseDenial(command);
93
+ }
94
+
83
95
  export default function builderGuard(pi: ExtensionAPI): void {
84
96
  pi.on("tool_call", (event) => {
85
97
  // bash only: the deny list is shell-command shaped; edit/write tools are
@@ -90,7 +102,7 @@ export default function builderGuard(pi: ExtensionAPI): void {
90
102
 
91
103
  // Test the command string itself, never the JSON-rendered whole input —
92
104
  // a payload that merely mentions `git commit` in prose must not match.
93
- const denial = gitWriteDenial(command) ?? parseDenial(command);
105
+ const denial = guardDenial(command);
94
106
  if (denial) {
95
107
  return {
96
108
  block: true,
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Space
4
4
  module Architect
5
- VERSION = "9.4.0"
5
+ VERSION = "9.5.0"
6
6
  end
7
7
  end
@@ -9,6 +9,7 @@ require_relative "space_architect/gate_lint"
9
9
  require_relative "space_architect/gate_evaluator"
10
10
  require_relative "space_architect/architect_project"
11
11
  require_relative "space_architect/skill_installer"
12
+ require_relative "space_architect/agent_installer"
12
13
  require_relative "space_architect/bug_report"
13
14
  require_relative "space_architect/research"
14
15
  require_relative "space_architect/cli"
@@ -140,11 +140,12 @@ the iteration verdicts, the integrated diff, and the BRIEF — to a fresh
140
140
  paste-and-run block (cd, `git push -u origin project/<slug>`, `gh pr
141
141
  create`). Releases of the space-architect gem itself are tag-driven
142
142
  (annotated `vX.Y.Z` tag on main → GitHub Action publishes to rubygems).
143
- Post-landing rollout for spaces running the loop's own tooling: rebuild +
144
- force-install the gem from the project branch, `architect install-skills`,
145
- `architect install-extensions`, `architect install-agents` — a config that
146
- moved ahead of the installed CLI rejects every command (the stale-gem
147
- trap). No push, no `gh` call by you; the human runs the block when ready.
143
+ Post-landing rollout for spaces running the loop's own tooling: once the
144
+ release is out (or rebuilt + force-installed from the project branch), run
145
+ `architect install` — the one-shot machine install: skills (pi by default),
146
+ the architect-loop extension, then the role playbooks. A config that moved
147
+ ahead of the installed CLI rejects every command (the stale-gem trap). No
148
+ push, no `gh` call by you; the human runs the block when ready.
148
149
 
149
150
  ## Iteration surgery (interactive path)
150
151
 
@@ -239,7 +240,8 @@ architect looplog [--list|--replay|--all|--space PATH]
239
240
  Follow every active loop run in this space, live (--list: inventory only)
240
241
  architect runlog [FILE]
241
242
  Pretty-print a pi run.jsonl (FILE or stdin; RUNLOG_LIMIT, NO_COLOR honored)
242
- architect install-skills bundled skills for a harness (--provider)
243
+ architect install one-shot: skills (--provider, default pi) + extension + role playbooks
244
+ architect install-skills bundled skills for a harness (--provider, default pi)
243
245
  architect install-extensions reconcile the architect-loop pi extension package
244
246
  architect install-agents role playbooks to ~/.pi/agent/agents (pi)
245
247
  architect bug-report prefilled GitHub issue template for the CLI
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: 9.4.0
4
+ version: 9.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Eric Jacobs
@@ -151,6 +151,7 @@ files:
151
151
  - README.md
152
152
  - exe/architect
153
153
  - lib/space_architect.rb
154
+ - lib/space_architect/agent_installer.rb
154
155
  - lib/space_architect/architect_project.rb
155
156
  - lib/space_architect/bug_report.rb
156
157
  - lib/space_architect/cli.rb