space-architect 9.2.1 → 9.4.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.
@@ -1,274 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "zlib"
4
- require "tempfile"
5
- require "json"
6
- require "async/http/client"
7
- require "async/http/endpoint"
8
- require "protocol/http/body/readable"
9
-
10
- module Space::Architect
11
- # Gem-side sender for the space-server import mouth: packs the current space
12
- # into a gzipped tar and POSTs it to <host>/spaces/import
13
- # (Content-Type: application/gzip, Bearer auth). A 200 response is the
14
- # server's import summary {id:, slug:, iterations:, artifacts:, runs:};
15
- # any non-200 raises Space::Core::Error carrying the server's error body.
16
- class SpacePush
17
- IMPORT_PATH = "/spaces/import"
18
- CONTENT_TYPE = "application/gzip"
19
-
20
- def initialize(host, token, client: nil)
21
- @host = host.chomp("/")
22
- @token = token
23
- @client = client
24
- end
25
-
26
- # POSTs a packed bundle (a gzip tar path) per the frozen import contract
27
- # and returns the parsed summary hash. Raises Space::Core::Error on any
28
- # non-200 — the CLI's handle_errors turns that into a stderr message and
29
- # a nonzero exit.
30
- def push(bundle_path)
31
- Sync do
32
- with_client do |c|
33
- body = file_body(bundle_path)
34
- begin
35
- response = c.post(IMPORT_PATH, headers: headers, body: body)
36
- parse_response(response)
37
- ensure
38
- body.close
39
- end
40
- end
41
- end
42
- rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Errno::ENETUNREACH, SocketError => e
43
- raise Space::Core::Error, "POST #{IMPORT_PATH} failed: #{e.message} (host: #{@host})"
44
- end
45
-
46
- private
47
-
48
- def with_client
49
- if @client
50
- yield @client
51
- else
52
- Async::HTTP::Client.open(Async::HTTP::Endpoint.parse(@host)) { |c| yield c }
53
- end
54
- end
55
-
56
- def headers
57
- [
58
- ["authorization", "Bearer #{@token}"],
59
- ["content-type", CONTENT_TYPE]
60
- ]
61
- end
62
-
63
- # Streams the bundle off disk — the bundle is never slurped into memory.
64
- # StreamBody's length drives the content-length on the wire (HTTP/1) and
65
- # the HTTP/2 pseudo-header, so it must not also be set as a header.
66
- def file_body(bundle_path)
67
- StreamBody.new(File.open(bundle_path, "rb"), File.size(bundle_path))
68
- end
69
-
70
- def parse_response(response)
71
- status = response.status
72
- body = response.read || ""
73
- raise Space::Core::Error, "POST #{IMPORT_PATH} failed (#{status}): #{body[0, 200]}" unless status == 200
74
-
75
- JSON.parse(body)
76
- end
77
-
78
- # A Protocol::HTTP body that lazily reads chunks off an IO, so the HTTP
79
- # client streams the request body while the bundle stays on disk.
80
- class StreamBody < Protocol::HTTP::Body::Readable
81
- CHUNK_SIZE = 64 * 1024
82
-
83
- def initialize(io, length)
84
- @io = io
85
- @length = length
86
- end
87
-
88
- attr :length
89
-
90
- def read
91
- @io.read(CHUNK_SIZE)
92
- end
93
-
94
- def close(error = nil)
95
- @io.close unless @io.closed?
96
- super
97
- end
98
- end
99
-
100
- # Packs a space into a gzipped tar on disk, streaming — neither the tar
101
- # nor the space's files are held in memory.
102
- #
103
- # Bundle cut (correctness-critical): EXCLUDE repos/, the lanes' worktrees
104
- # at build/<id>-<lane>/wt, tmp/, node_modules/ anywhere, .pi/, and *.log
105
- # files. INCLUDE everything else — the importer's read set is space.yaml,
106
- # .git/ (history carries iteration timestamps), architecture/, notes/,
107
- # and build/<id>-<lane>/{prompt,report}.md + run.jsonl.
108
- class Packer
109
- BLOCK_SIZE = 512
110
- PRUNED_DIRS = %w[repos tmp .pi node_modules]
111
- WORKTREE_MATCH = %r{\Abuild/[^/]+/wt\z}
112
-
113
- Entry = Struct.new(:name, :path, :type)
114
-
115
- def initialize(root)
116
- @root = root.to_s
117
- end
118
-
119
- # Streams the space into a fresh tmpfile and returns its path. The
120
- # caller owns the tmpfile (unlink it when done).
121
- def pack
122
- tmp = Tempfile.create(["architect-push", ".tar.gz"])
123
- tmp.binmode
124
- Zlib::GzipWriter.wrap(tmp) do |gz|
125
- writer = TarWriter.new(gz)
126
- walk { |entry| add(writer, entry) }
127
- writer.finish
128
- end
129
- tmp.close
130
- tmp.path
131
- rescue
132
- tmp.close
133
- File.unlink(tmp.path) rescue nil
134
- raise
135
- end
136
-
137
- private
138
-
139
- def add(writer, entry)
140
- if entry.type == :dir
141
- writer.write_dir(entry.name, mtime: File.mtime(entry.path))
142
- else
143
- writer.write_file(entry.path, entry.name,
144
- mode: File.stat(entry.path).mode & 0o7777, mtime: File.mtime(entry.path))
145
- end
146
- end
147
-
148
- def walk(&)
149
- walk_dir("", &)
150
- end
151
-
152
- def walk_dir(rel, &)
153
- dir = rel.empty? ? @root : File.join(@root, rel)
154
- Dir.children(dir).sort.each do |name| # paths:exempt — the dotfile-inclusive walk is deliberate (.git must ship); pruned subtrees (repos/, worktrees) are skipped before descending, so their bulk is never enumerated and the walk streams
155
- child = rel.empty? ? name : "#{rel}/#{name}"
156
- abs = File.join(@root, child)
157
- stat = File.lstat(abs)
158
-
159
- if stat.directory?
160
- next if pruned?(child)
161
-
162
- yield Entry.new("#{child}/", abs, :dir)
163
- walk_dir(child, &)
164
- elsif stat.file?
165
- next if child.end_with?(".log")
166
-
167
- yield Entry.new(child, abs, :file)
168
- end
169
- # Symlinks and other oddities are skipped: the bundle carries regular
170
- # files and directories only, so it stays traversal-proof.
171
- end
172
- end
173
-
174
- def pruned?(rel)
175
- PRUNED_DIRS.include?(File.basename(rel)) || WORKTREE_MATCH.match?(rel)
176
- end
177
-
178
- # Minimal ustar writer — stdlib only, the Gemfile carries no tar gem.
179
- # Regular files and directories; file contents stream 512-byte-aligned.
180
- # Names longer than 100 bytes split across the ustar prefix field.
181
- class TarWriter
182
- BLOCK_SIZE = 512
183
- TYPE_FILE = "0"
184
- TYPE_DIR = "5"
185
-
186
- def initialize(io)
187
- @io = io
188
- end
189
-
190
- def write_dir(name, mtime:)
191
- write_header(name, mode: 0o755, mtime: mtime, size: 0, type: TYPE_DIR)
192
- end
193
-
194
- def write_file(path, name, mode:, mtime:)
195
- size = File.size(path)
196
- write_header(name, mode: mode, mtime: mtime, size: size, type: TYPE_FILE)
197
- IO.copy_stream(path, @io)
198
- pad = (BLOCK_SIZE - (size % BLOCK_SIZE)) % BLOCK_SIZE
199
- @io.write("\0" * pad) unless pad.zero?
200
- end
201
-
202
- # Two zero blocks terminate the archive.
203
- def finish
204
- @io.write("\0" * (BLOCK_SIZE * 2))
205
- end
206
-
207
- private
208
-
209
- def write_header(name, mode:, mtime:, size:, type:)
210
- name = name.b
211
- split = split_name(name)
212
- if split
213
- base, prefix = split
214
- else
215
- # Not representable via the ustar prefix field — fall back to a
216
- # GNU 'L' long-name entry, which every mainstream reader honors.
217
- write_long_name(name)
218
- base, prefix = name[0, 100], nil
219
- end
220
-
221
- header = String.new(encoding: Encoding::BINARY)
222
- header << base.ljust(100, "\0")
223
- header << octal(mode, 8)
224
- header << octal(0, 8) # uid — the bundle is anonymous
225
- header << octal(0, 8) # gid
226
- header << octal(size, 12)
227
- header << octal(mtime.to_i, 12)
228
- header << " " * 8 # checksum placeholder
229
- header << type
230
- header << "\0" * 100 # linkname
231
- header << "ustar\0" # magic
232
- header << "00" # version
233
- header << "\0" * 32 # uname
234
- header << "\0" * 32 # gname
235
- header << "\0" * 8 # devmajor
236
- header << "\0" * 8 # devminor
237
- header << (prefix || "").ljust(155, "\0")
238
- header << "\0" * (BLOCK_SIZE - header.bytesize)
239
- raise "tar header overflow: #{name}" if header.bytesize != BLOCK_SIZE
240
-
241
- header[148, 8] = format("%06o\0 ", header.sum)
242
- @io.write(header)
243
- end
244
-
245
- # GNU 'L' long-name entry: the full name NUL-terminated as the entry
246
- # body, then the real header carrying the name's first 100 bytes.
247
- def write_long_name(name)
248
- content = "#{name}\0"
249
- write_header("././@LongLink", mode: 0o644, mtime: Time.now, size: content.bytesize, type: "L")
250
- @io.write(content)
251
- pad = (BLOCK_SIZE - (content.bytesize % BLOCK_SIZE)) % BLOCK_SIZE
252
- @io.write("\0" * pad) unless pad.zero?
253
- end
254
-
255
- # [base, prefix] split across the ustar name (100) and prefix (155)
256
- # fields at a "/" boundary, or nil when the name cannot be split and
257
- # needs a GNU long-name entry.
258
- def split_name(name)
259
- return [name, nil] if name.bytesize <= 100
260
-
261
- min_cut = [name.bytesize - 101, 0].max # the name field must fit in 100 bytes
262
- cut = name.index("/", min_cut)
263
- return nil if cut.nil? || cut > 155
264
-
265
- [name[(cut + 1)..], name[0, cut]]
266
- end
267
-
268
- def octal(value, width)
269
- format("%0#{width - 1}o\0", value)
270
- end
271
- end
272
- end
273
- end
274
- end
@@ -1,114 +0,0 @@
1
- ---
2
- name: architect-plan
3
- description: "Plan work for the architect loop and launch it as an autonomous coordinator run (/loop). Use when the user's intention is to START a loop: planning iterations, writing the plan doc (notes/plan-*.md), pinning role routing, launching the coordinator, and watching it run. For general architecture work without launching a loop (arbitration, judging, landing, iteration surgery, conversation), use /architect instead."
4
- ---
5
-
6
- # Architect Plan
7
-
8
- You are the ARCHITECT planning a coordinator run. The deliverable of this
9
- skill is exactly one thing: a committed plan doc and a launched coordinator.
10
- The loop's roles (spec → builders → judge, driven by the coordinator) do
11
- everything after that — you never absorb a builder wakeup.
12
-
13
- Read `/architect` (the main skill) first for roles, routing, and the hard
14
- rules. This skill covers only the start.
15
-
16
- ## Procedure
17
-
18
- 1. **Ground (never skip).**
19
- - `architect status` — iteration/verdict state; anything in flight?
20
- - `architect looplog --list` — is a loop already running? Fresh transcripts
21
- mean a live loop: don't double-start; report and ask.
22
- - Read `architecture/ARCHITECT.md` (TOC + backlog), `architecture/BRIEF.md`
23
- (the durable §-contract — cite it as BRIEF §N), the in-flight iteration
24
- file if any, and the latest `notes/spec-digest-*.md` (the previous spec's
25
- handoff: verification commands actually used, repo seams, house
26
- conventions, open unknowns — carry its facts forward or they are lost).
27
- - Read the target repo's `AGENTS.md`/`CLAUDE.md` → `README.md` → docs;
28
- confirm the exact verification gate command.
29
- - Learn the verification gate (test/lint/typecheck/build) from docs or CI
30
- config — the plan's acceptance direction must name it.
31
- 2. **Scale the work.** Trivial fixes don't need the loop — say so and do it
32
- inline. One PR-sized iteration = one ENTRY. Multi-iteration plans get
33
- ordered ENTRYs (the coordinator works them in order, skipping finished
34
- ones). If the open question is too uncertain to build, make the ENTRY a
35
- spike (investigate-only; deliverable is a recommendation — see /architect).
36
- 3. **Write the plan doc** to `notes/plan-<UTC timestamp>-<name>.md`:
37
-
38
- ```markdown
39
- # Plan — <name> (<one line>)
40
-
41
- Operator plan <date>. <Repos/lanes in scope.>
42
-
43
- Standing context the spec needs: prior iterations this builds on, routing
44
- pins, and the post-landing follow-ups (outside-the-loop work the plan
45
- deliberately excludes — name it so it isn't lost).
46
-
47
- ## ENTRY N — iteration <name> (repo: <repo>, <lane count> lane(s))
48
-
49
- **Objective.** What to build and why — the reason, not just the ask. Cite
50
- BRIEF §N. If this ENTRY pins model/thinking for a role, say it here (the
51
- coordinator passes it in that spawn's model field).
52
-
53
- **Grounds to read.** Exact paths: research/bug reports, source files being
54
- ported, seam files, prior verdicts. The spec reads these before writing a
55
- word.
56
-
57
- **Acceptance direction (spec refines into frozen AC).** The properties the
58
- finished iteration must have — at the level the spec can freeze, not
59
- pre-written gates (freezing is the spec's job).
60
-
61
- **Constraints.** Repos; files that must not change; out-of-scope;
62
- house-style expectations; what the lane must NOT delete or move (say so
63
- when a port is replacing something that lives elsewhere).
64
- ```
65
-
66
- Each ENTRY must be self-contained — the spec agent receives the entry
67
- verbatim and nothing else. Put the verification gate command in ENTRY
68
- constraints or grounds.
69
- 4. **Pin routing if needed.** This plan's models come from `architect.yaml`
70
- (space root overrides `~/.config/space-architect/architect.yaml` per role).
71
- A one-plan pin goes in the plan header/ENTRY (the coordinator passes it in
72
- that spawn's model field); a standing change edits `architect.yaml` —
73
- routing rides the spawn contract and `/loop` resolves it at launch, so
74
- there are no generated agent files to hand-sync (the role playbooks are
75
- static, user-level, installed once by `architect install-agents`).
76
- 5. **Commit the plan doc** to the space — an uncommitted plan is scratch, and
77
- decision-bearing documents live in `notes/` (not-committed = didn't
78
- happen).
79
- 6. **Launch.** From an interactive pi session at the space root:
80
-
81
- ```
82
- /loop --from notes/plan-<ts>-<name>.md
83
- ```
84
-
85
- `/loop` refuses an empty plan, resolves the routing config (validates
86
- every pin against the model registry — a pin it doesn't know throws
87
- loudly, before any spawn — and injects the per-role pins into the
88
- coordinator's task), and spawns `architect-coordinator` detached — your
89
- session may end; the coordinator (12h ceiling) works the plan, narrating
90
- every settled event to `notes/narrative-*.md`. If pi-subagents' RPC is not
91
- up in the session, /loop says so — spawn the coordinator directly instead
92
- (agent `architect-coordinator`, the space as cwd, the plan text verbatim as
93
- the task, the coordinator's model pin from `architect.yaml`, `timeoutMs`
94
- 43200000).
95
- 7. **Report the handoff, then stop.** Tell the human: the plan path, the
96
- coordinator run id, and how to watch —
97
- `architect looplog` (live), `--list` (inventory),
98
- `architect status`, `architect cost`. Escalations route through the
99
- coordinator to your supervisor seat: rule and the coordinator steers; if
100
- your session won't stay alive, say so — a coordinator with no listener
101
- parks the question in `notes/escalation-<ts>.md` and stops via the exception
102
- path rather than improvising.
103
-
104
- ## Failure modes to check before launching
105
-
106
- - **Frozen-region edits** — never edit `architecture/` directly; the plan
107
- steers, the spec writes sections via the CLI.
108
- - **Stale base** — outside-the-loop commits that landed on a repo's main since
109
- the last provision are fine (`architect provision` fast-forward-syncs
110
- `project/<slug>` from the default branch or refuses loudly on divergence);
111
- a deliberately diverged integration branch must be named in the plan.
112
- - **Stale gem / extension** — if the plan exercises CLI behavior newer than
113
- the installed gem, note the rebuild in standing context: a config that
114
- moved ahead of the installed CLI rejects every command.
@@ -1,144 +0,0 @@
1
- ---
2
- name: architect-vocabulary
3
- description: >
4
- Load the Architect system's vocabulary and a short "where you are"
5
- orientation — space, project, iteration, lane, brief, builder, architect,
6
- gate, freeze, verdict, research, variant set — for when you're standing in a
7
- space-architect workspace (or working on the skill itself) and need the terms
8
- understood in conversation but do NOT want to run the loop. Reference only:
9
- it does not dispatch builders, freeze, judge, or write iteration files.
10
- Invoke as /architect-vocabulary.
11
- ---
12
-
13
- # Architect Vocabulary
14
-
15
- This skill loads **terminology and orientation only**. It is the glossary, not
16
- the loop.
17
-
18
- ## What this skill is — and isn't
19
-
20
- - **Is:** the shared vocabulary of `space-architect` plus a quick orientation to
21
- where things live, so these terms are understood for the rest of the session.
22
- - **Isn't:** the build loop. Do **not** run `architect new` / `freeze` /
23
- `dispatch` / `integrate`, do **not** write or edit anything under
24
- `architecture/`, and do **not** render verdicts on builder work. That is the
25
- separate **`/architect`** skill — invoke it deliberately when you actually
26
- want to run the loop. Read-only `architect` commands (`status`, `show`) are
27
- fine here.
28
-
29
- ## Vocabulary
30
-
31
- **Roles**
32
-
33
- - **architect** — the judgment role (a strong reasoning model, or a human, in
34
- judgment mode): arbitrates disagreements, writes and freezes iteration files,
35
- calls kill/continue, merges builder output. Never writes implementation code.
36
- - **builder** — the implementation role: a cheaper model run headless via
37
- `architect dispatch` (reference harness: `pi -p`), one per lane in its own
38
- worktree. Reports
39
- raw evidence; never grades its own work; never edits `architecture/`.
40
-
41
- **The workspace**
42
-
43
- - **space** — a task-scoped workspace directory holding repos, notes, and
44
- artifacts under one root. `architect` finds it by walking up from `$PWD` to the
45
- nearest `space.yaml`.
46
- - **space.yaml** — the space's identity file: id, title, status, repos, notes,
47
- tags, plus the `project:` block (project state — iterations, freeze shas,
48
- lanes).
49
- - **project** — an Architect Loop instance living inside a space; spans the
50
- repos under `repos/`.
51
-
52
- **The unit of work**
53
-
54
- - **iteration** — one PR-sized unit of work, captured as a single self-contained
55
- file `architecture/I<NN>-<name>.md`, grown section by section. Its sections:
56
- - **Grounds** — *why*: research/brief distilled (optional).
57
- - **Specification** — *what/how*: the full delegation contract.
58
- - **Acceptance Criteria** — *proof*: prose conditions (AC1, AC2, …) the
59
- architect judges against, plus a fenced ` ```gates ` block of runnable checks;
60
- this is what gets frozen.
61
- - **Builder Prompt** — the exact lane-prompt(s) dispatched.
62
- - **Builder Report** — raw evidence, transcribed verbatim from build scratch.
63
- - **Verdict** — rulings + per-AC PASS/FAIL/INVALID + KILL/CONTINUE.
64
- - **lane** — a parallel slice of an iteration (1–4 per iteration), each
65
- declaring a disjoint target repo + file-touch set. Lanes in different repos are
66
- inherently disjoint; same-repo lanes that overlap files run as one. Each runs
67
- in its own worktree under `build/<id>-<lane>/`.
68
- - **worktree** — the isolated git worktree a lane builds in, off the target
69
- repo's base commit, so lanes never collide.
70
- - **dispatch** — launching a fresh headless builder for a lane (`architect
71
- dispatch <iteration> <lane>`), streaming output to `build/<id>-<lane>/run.jsonl`.
72
-
73
- **Contracts and checkpoints**
74
-
75
- - **brief** (`architecture/BRIEF.md`) — the durable, §-numbered project contract
76
- that spans iterations; frozen at the project level and cited as **BRIEF §N**.
77
- - **ARCHITECT.md** (`architecture/ARCHITECT.md`) — the cross-iteration index /
78
- table of contents and project-wide state.
79
- - **freeze** ❄️ — committing the frozen region (Grounds / Specification /
80
- Acceptance Criteria) *before* dispatch (`architect freeze`). Records the
81
- **freeze_sha** in `space.yaml`; any later change to those sections is an
82
- automatic iteration FAIL.
83
- - **gate** — a structured entry in the ` ```gates ` block: `id`, `ac`, `cmd`,
84
- optional `cwd`, and an `expect` hash (exit_code / stdout_match / threshold).
85
- `architect gate` runs each gate's `cmd` and streams raw output — it is a runner,
86
- never a judge. `architect freeze` lints the gates block before committing.
87
-
88
- **Outcomes**
89
-
90
- - **verdict** — the architect's ruling on an iteration, written after evidence:
91
- - per-criterion: **PASS** / **FAIL** / **INVALID** (INVALID = not measured the
92
- way the gate specifies).
93
- - iteration-level: **KILL** / **CONTINUE**.
94
- - **variant set** — an iteration built as multiple `(harness, model)` lanes over
95
- one frozen spec, judged head-to-head against the same Acceptance Criteria; the
96
- winner is selected with the human in the loop, not unilaterally.
97
-
98
- **Research**
99
-
100
- - **research** — two scales:
101
- - **discovery scale** (brainstorming, technology selection, state-of-the-art)
102
- → the **`/architect-research`** skill: a scout maps the topic, the
103
- orchestrator designs parallel researcher **lanes**, claims are verified
104
- against sources, and the synthesis distills into a brief §section or an
105
- iteration's Grounds.
106
- - **iteration scale** → an inline fan-out run only when an iteration needs
107
- facts the repo doesn't already have.
108
-
109
- **Repos**
110
-
111
- - **evergreen** / copy-on-write / **`src` engine** — repo provisioning: when an
112
- up-to-date local copy exists under `src_dir`, `architect` copies it into
113
- the space (copy-on-write on APFS) instead of cloning over the network. The
114
- `src` engine (`space-src`) keeps those evergreen checkouts tended.
115
-
116
- ## Where you are
117
-
118
- A space's directory layout:
119
-
120
- ```text
121
- space.yaml # identity + project state (the project: block)
122
- README.md
123
- repos/ # the repos the project spans
124
- notes/ # scratch, prompts, logs
125
- architecture/ # ARCHITECT.md index + I<NN>-<name>.md iteration files (+ BRIEF.md)
126
- build/ # lane worktrees + scratch: build/<id>-<lane>/
127
- tmp/ # workspace-local temp — use instead of /tmp
128
- ```
129
-
130
- Safe **read-only** commands to orient yourself (these don't run the loop):
131
-
132
- ```sh
133
- architect status # project state: iterations, freeze shas, lanes, verdicts
134
- space show # the space you're standing in (space-cadet binary)
135
- space list # all your spaces
136
- ```
137
-
138
- ## Maintenance
139
-
140
- This glossary is a **self-contained copy** of terms defined canonically in the
141
- `architect` skill (`SKILL.md`) and the project `README.md`. It is installed as an
142
- isolated skill, so it can't reference those at runtime — when the vocabulary
143
- changes there, re-read this file against them and update it to keep the two from
144
- drifting.