space-architect 9.3.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: 4fd0baee9b2db69fa1ef96c1b4993f75a8912b5b0898d012c13036a379e32a7f
4
- data.tar.gz: 582172fa5c61e4a9419409cafdfa8701ca096a2621527141099944f3261c6592
3
+ metadata.gz: 4de640b748008dc4484903c88d496366528cb9eb294002abad5bc02547816198
4
+ data.tar.gz: 4aa87deb60eee6dc8d768d6f333204137d7ddb49a3e94640263fce5711623e3e
5
5
  SHA512:
6
- metadata.gz: 59dd000d01b8fd0b42729d87f7f843f93690b0fbccf927e127426f352fa79aada6078a885b54e3342d3c901500fde93338be3198b77e871df85f7b28ef816f9e
7
- data.tar.gz: 29f88c1fdec961ed7b53d9d9e4a3a3c156cadac2da157786bea28fca4b20b09cb786ab195d5eedf0877e11c901a2d140d66347bbee3fc52570a4a68225b78284
6
+ metadata.gz: 5e383f34c36693c6ad9290ec9db84815835545676c194d4c06d79d630c6bc4f98ba6b1c7072570c60b4ce3df2b782d0a6e064b0d1ed5d31fdd9baacc6dbe0342
7
+ data.tar.gz: b957f8f1a303a9a2c88ad0c569a8ce8b572740dfffb1da6aa203b0e66fb15f1984d711479549c6d1a35ae7ece15a502f3f48994f5614bf09a06346b3392b129a
data/CHANGELOG.md CHANGED
@@ -5,6 +5,64 @@ 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
+
33
+ ## [9.4.0] - 2026-10-17
34
+
35
+ ### Added
36
+
37
+ - **The skill family.** `/architect` is now the inline working mode — work
38
+ outside the loop at a conversational cadence (UI, ops, small fixes), with
39
+ the space as the workspace (notes kept, `ARCHITECT.md` updated, commits
40
+ when the work calls for them) and the vocabulary glossary folded in.
41
+ **`/architect-discuss`** carries the judgment seat: exploration,
42
+ arbitration, judging finished runs, landing, iteration surgery.
43
+ **`/architect-status`** reports state read-only from CLI output and the
44
+ newest notes. The coordinator launch is agent-owned end to end:
45
+ **`/architect-action`** (renamed from `/architect-plan`) launches via the
46
+ new `architect_loop` pi-extension tool (jetpks/pi-extensions) — the
47
+ agent-side equivalent of `/loop`, sharing one spawn path so the two cannot
48
+ drift, awaiting the run id, then self-compacting the launching session
49
+ into a small on-call escalation seat.
50
+
51
+ ### Changed
52
+
53
+ - The `install-skills` help text, `docs/DESIGN.md`, and
54
+ `docs/reference/project.md` describe the five-skill family; the
55
+ `/architect-discuss` routing section now describes the real dual
56
+ agent-file mechanism (per-space `.pi/agents/` generation with resolved
57
+ pins, plus user-level `install-agents` playbooks as the headless/
58
+ untrusted fallback) instead of claiming nothing generates into the space.
59
+
60
+ ### Removed
61
+
62
+ - **`/architect-plan`** (renamed to `/architect-action`) and
63
+ **`/architect-vocabulary`** (folded into `/architect`); the `dispatch.md`
64
+ and `research.md` companion references moved to the discuss skill.
65
+
8
66
  ## [9.3.0] - 2026-10-17
9
67
 
10
68
  ### Removed
@@ -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
@@ -1713,7 +1713,7 @@ module Space::Architect
1713
1713
  # two paths looked at, and the default in use.
1714
1714
  def default_model_inform
1715
1715
  "model: no architect.yaml found (looked at #{Routing.space_path(space.path)} + #{Routing.global_path}) — " \
1716
- "using default '#{Harness::DEFAULT_MODEL}'"
1716
+ "using default '#{Harness::DEFAULT_MODEL}' #{Routing::INIT_HINT}"
1717
1717
  end
1718
1718
 
1719
1719
  # --prompt: the caller authors the lane prompt anywhere (a fresh scratch file) and
@@ -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
@@ -169,7 +170,9 @@ module Space::Architect
169
170
  # Legacy lanes store no model — resolve the config layer once so the
170
171
  # display shows what they would actually route, and mark the bare
171
172
  # constant as the last resort it is, not ordinary behavior.
172
- config_model = Routing.for_space(sp.path)&.dig("builder", "model")
173
+ # Fully qualified: the Architect::Routing subcommand namespace
174
+ # (I09) lexically shadows Space::Architect::Routing inside this module.
175
+ config_model = Space::Architect::Routing.for_space(sp.path)&.dig("builder", "model")
173
176
 
174
177
  terminal.say "Project status: #{block['status'] || '(none)'}"
175
178
  terminal.say "Current iteration: #{block['current_iteration'] || '(none)'}"
@@ -1060,14 +1063,14 @@ module Space::Architect
1060
1063
  end
1061
1064
 
1062
1065
  class InstallSkills < BaseCommand
1063
- desc "Install bundled skills (architect, architect-research, architect-vocabulary) for a harness"
1066
+ desc "Install bundled skills (architect, architect-discuss, architect-action, architect-status, architect-research) for a harness"
1064
1067
  phase 53, "Project"
1065
- 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)"
1066
1069
  option :project, type: :boolean, default: false, desc: "Install to CWD instead of global"
1067
1070
  option :force, type: :boolean, default: false, desc: "Overwrite existing skills that differ"
1068
1071
  option :dry_run, type: :boolean, default: false, desc: "Print what would happen without writing files"
1069
1072
 
1070
- 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)
1071
1074
  setup_terminal(**opts.slice(:color, :colors))
1072
1075
  handle_errors do
1073
1076
  result = SkillInstaller.install(provider, project: project, force: force,
@@ -1100,6 +1103,111 @@ module Space::Architect
1100
1103
  end
1101
1104
  end
1102
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
+
1161
+ module Routing
1162
+ # I09: `architect routing init` — the self-service cure for the
1163
+ # missing-config wall (default_model_inform's hint names this command).
1164
+ # Writes Routing::TEMPLATE verbatim and round-trips the write through
1165
+ # Routing.parse before claiming success; the routing machinery itself
1166
+ # is untouched — the command only writes a file it already accepts.
1167
+ class Init < BaseCommand
1168
+ desc "Write a commented starter architect.yaml (space root by default; --global for the machine-wide config)"
1169
+ phase 53, "Project"
1170
+ argument :space, required: false, desc: "Space identifier (default: $PWD)"
1171
+ option :global, type: :boolean, default: false,
1172
+ desc: "Write ~/.config/space-architect/architect.yaml (the machine-wide config; SPACE_ARCHITECT_GLOBAL_CONFIG overrides the path) instead of the space root, creating parent directories as needed"
1173
+
1174
+ def call(space: nil, global: false, **opts)
1175
+ setup_terminal(**opts.slice(:color, :colors))
1176
+ handle_errors do
1177
+ target =
1178
+ if global
1179
+ raise Space::Core::Error, "--global and an explicit space argument are mutually exclusive" if space
1180
+ # Routing.global_path is the env-overridable canonical path — never re-derive it here.
1181
+ path = Space::Architect::Routing.global_path
1182
+ FileUtils.mkdir_p(path.dirname)
1183
+ path
1184
+ else
1185
+ # The house walk-up: empty identifier looks for space.yaml from $PWD upward.
1186
+ found = store.find(space).value_or(nil)
1187
+ unless found
1188
+ raise Space::Core::Error,
1189
+ "no space.yaml found upward from #{Dir.pwd} — run `architect routing init --global` to scaffold the machine-wide config instead"
1190
+ end
1191
+ Space::Architect::Routing.space_path(found.path)
1192
+ end
1193
+
1194
+ raise Space::Core::Error, "architect.yaml already exists: #{target} — routing init never overwrites" if target.exist?
1195
+
1196
+ File.write(target, Space::Architect::Routing::TEMPLATE)
1197
+ # A scaffold that fails its own parser (or misses a required role)
1198
+ # is a bug — validate the round-trip, let the error escape loud.
1199
+ parsed = Space::Architect::Routing.parse(File.read(target), target)
1200
+ missing = Space::Architect::Routing::REQUIRED_ROLES - parsed.keys
1201
+ raise Space::Core::Error, "scaffolded #{target} is missing required roles: #{missing.join(', ')}" if missing.any?
1202
+
1203
+ terminal.say "Wrote starter routing config: #{terminal.path(target.to_s)}"
1204
+ terminal.say "Edit the example/REPLACE_ME models; the next command or /loop picks the file up."
1205
+ CLI.record_outcome(Outcome.new(exit_code: 0))
1206
+ end
1207
+ end
1208
+ end
1209
+ end
1210
+
1103
1211
  module Worktree
1104
1212
  class Add < BaseCommand
1105
1213
  desc "Create a worktree for a lane"
@@ -1326,8 +1434,10 @@ Space::Architect::CLI::Registry.register "evidence", Space::Architect::CLI::Arc
1326
1434
  Space::Architect::CLI::Registry.register "merge", Space::Architect::CLI::Architect::Merge
1327
1435
  Space::Architect::CLI::Registry.register "integrate", Space::Architect::CLI::Architect::Integrate
1328
1436
  Space::Architect::CLI::Registry.register "gate", Space::Architect::CLI::Architect::Gate
1437
+ Space::Architect::CLI::Registry.register "install", Space::Architect::CLI::Architect::Install
1329
1438
  Space::Architect::CLI::Registry.register "install-skills", Space::Architect::CLI::Architect::InstallSkills
1330
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
1331
1441
  Space::Architect::CLI::Registry.register "runlog", Space::Architect::CLI::Architect::Runlog
1332
1442
  Space::Architect::CLI::Registry.register "looplog", Space::Architect::CLI::Architect::Looplog
1333
1443
  Space::Architect::CLI::Registry.register "bug-report", Space::Architect::CLI::Architect::BugReport
@@ -1340,6 +1450,9 @@ Space::Architect::CLI::Registry.register "worktree" do |wt|
1340
1450
  wt.register "remove", Space::Architect::CLI::Architect::Worktree::Remove
1341
1451
  wt.register "list", Space::Architect::CLI::Architect::Worktree::List
1342
1452
  end
1453
+ Space::Architect::CLI::Registry.register "routing" do |r|
1454
+ r.register "init", Space::Architect::CLI::Architect::Routing::Init
1455
+ end
1343
1456
  Space::Architect::CLI::Registry.register "variant" do |v|
1344
1457
  v.register "add", Space::Architect::CLI::Architect::Variant::Add
1345
1458
  v.register "promote", Space::Architect::CLI::Architect::Variant::Promote
@@ -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,
@@ -22,6 +22,49 @@ module Space::Architect
22
22
  REQUIRED_ROLES = %w[spec builder judge coordinator narrator].freeze
23
23
  ENTRY_KEYS = %w[model thinking].freeze
24
24
 
25
+ # I09: the `architect routing init` scaffold — the exact bytes the command
26
+ # writes, living beside the parsers that must accept it. Example models are
27
+ # loud REPLACE_MEs; the header comment carries both locations and the merge
28
+ # order so the file is self-documenting. Byte-exact contract: the extension
29
+ # lane's tests carry an inline copy, and the command round-trips the write
30
+ # through parse below — a template the machinery rejects is a bug.
31
+ TEMPLATE = <<~'YAML'.freeze
32
+ # architect.yaml — space-architect loop role routing, scaffolded by `architect routing init`.
33
+ # Locations (merged in this order; the space-local entry wins per role):
34
+ # global: ~/.config/space-architect/architect.yaml (machine-wide)
35
+ # local: architect.yaml beside this space's space.yaml (this file)
36
+ # A local entry overrides the global entry for THAT ROLE only; after the
37
+ # merge every agent role must be routed: spec, builder, judge, coordinator,
38
+ # narrator (architect is optional).
39
+ # Replace every example/REPLACE_ME below with a real provider/model-id
40
+ # (e.g. zai/glm-5.3). thinking is optional: off|minimal|low|medium|high|xhigh|max.
41
+ # Once saved, the next /loop or architect command picks the file up.
42
+ version: 1
43
+ roles:
44
+ architect:
45
+ model: example/REPLACE_ME
46
+ thinking: high
47
+ spec:
48
+ model: example/REPLACE_ME
49
+ thinking: high
50
+ builder:
51
+ model: example/REPLACE_ME
52
+ thinking: high
53
+ judge:
54
+ model: example/REPLACE_ME
55
+ thinking: high
56
+ coordinator:
57
+ model: example/REPLACE_ME
58
+ thinking: high
59
+ narrator:
60
+ model: example/REPLACE_ME
61
+ thinking: low
62
+ YAML
63
+
64
+ # The one canonical cure line appended to default_model_inform's last-resort
65
+ # inform — self-punctuating, appended after a single space by the caller.
66
+ INIT_HINT = "Create one: `architect routing init` (beside space.yaml) or `architect routing init --global` (~/.config/space-architect/architect.yaml).".freeze
67
+
25
68
  def self.global_path
26
69
  env = ENV["SPACE_ARCHITECT_GLOBAL_CONFIG"]
27
70
  return Pathname.new(env) unless env.nil?
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Space
4
4
  module Architect
5
- VERSION = "9.3.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"