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 +4 -4
- data/CHANGELOG.md +58 -0
- data/lib/space_architect/agent_installer.rb +127 -0
- data/lib/space_architect/architect_project.rb +1 -1
- data/lib/space_architect/cli/architect.rb +117 -4
- data/lib/space_architect/looplog.rb +27 -7
- data/lib/space_architect/pi/builder-guard.ts +13 -1
- data/lib/space_architect/routing.rb +43 -0
- data/lib/space_architect/version.rb +1 -1
- data/lib/space_architect.rb +1 -0
- data/skill/architect/SKILL.md +145 -319
- data/skill/architect-action/SKILL.md +123 -0
- data/skill/architect-discuss/SKILL.md +254 -0
- data/skill/{architect → architect-discuss}/dispatch.md +5 -4
- data/skill/{architect → architect-discuss}/research.md +2 -1
- data/skill/architect-research/SKILL.md +3 -2
- data/skill/architect-status/SKILL.md +55 -0
- metadata +7 -5
- data/skill/architect-plan/SKILL.md +0 -114
- data/skill/architect-vocabulary/SKILL.md +0 -144
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4de640b748008dc4484903c88d496366528cb9eb294002abad5bc02547816198
|
|
4
|
+
data.tar.gz: 4aa87deb60eee6dc8d768d6f333204137d7ddb49a3e94640263fce5711623e3e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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-
|
|
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: "
|
|
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: "
|
|
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:
|
|
170
|
-
|
|
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
|
-
|
|
175
|
-
|
|
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 =
|
|
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?
|
data/lib/space_architect.rb
CHANGED
|
@@ -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"
|