space-architect 9.0.0 → 9.1.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/README.md +32 -267
- data/lib/space_architect/architect_project.rb +90 -39
- data/lib/space_architect/cli/architect.rb +169 -4
- data/lib/space_architect/cli/research.rb +1 -1
- data/lib/space_architect/loop_cost.rb +592 -0
- data/lib/space_architect/research/supervisor.rb +9 -2
- data/lib/space_architect/routing.rb +149 -0
- data/lib/space_architect/version.rb +1 -1
- data/lib/space_architect.rb +1 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c2dfa94f98278929ea382e4411856749de3af270511f60760ee4e3bed83daf53
|
|
4
|
+
data.tar.gz: 33c9bf994eafc772c67399876826b77f685a7ac9e3417f38b37054d853b5479b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2b7c099ff9ffc9b1cf6e37d48a687b2c08c4f2c0463ea5f1e77d156f214bdf9e4f5d0059e1be9fa1247de440c50ea091ffb933db5f02c3bb8e2d8d75ed0f3996
|
|
7
|
+
data.tar.gz: 5ce698b9ed448dbab61db46302c5f51f4e0bd850c8748c052131ffea4ef718326a3a4de47562092ba782dcfcfa042c6dedfe1c56675119b9ba2d98e29caf5f49
|
data/README.md
CHANGED
|
@@ -5,288 +5,59 @@
|
|
|
5
5
|
> **The Architect Loop — structured judgment-and-build cycles for humans and their agents!** ✨🛰️
|
|
6
6
|
|
|
7
7
|
`space-architect` is the gem behind the **Architect Loop**: a structured
|
|
8
|
-
judgment-and-build cycle
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
judgment-and-build cycle in which a strong reasoning model (the *architect*)
|
|
9
|
+
plans, freezes acceptance criteria, and judges the work of a fleet of headless
|
|
10
|
+
AI *builders* — one per lane, each in its own git worktree. The loop runs inside
|
|
11
|
+
task-scoped **spaces**: directories that hold repos, notes, and artifacts under
|
|
12
|
+
one obvious filesystem root. Nothing lives only in a chat window — every
|
|
13
|
+
decision, spec, and verdict is committed to the space repo.
|
|
11
14
|
|
|
12
15
|
It installs **one executable**, `architect`. The spaces substrate and the
|
|
13
|
-
evergreen-checkout engine
|
|
16
|
+
evergreen-checkout engine are two sibling gems:
|
|
14
17
|
|
|
15
18
|
| Gem | Dependency | What it provides |
|
|
16
19
|
|-----|------------|------------------|
|
|
17
|
-
| **space-cadet** | hard | The spaces substrate (`Space::Core`): create, manage & containerize task-scoped workspaces — the `space` binary — plus the copy-on-write evergreen-checkout substrate
|
|
20
|
+
| **space-cadet** | hard | The spaces substrate (`Space::Core`): create, manage & containerize task-scoped workspaces — the `space` binary — plus the copy-on-write evergreen-checkout substrate repos provision from |
|
|
18
21
|
| **repo-tender** | soft | Evergreen clone tending — the `repo-tender` binary (with the deprecated `src` shim) — and the launchd agent the `architect sessions agent` commands drive. Install with `gem install repo-tender` when you want the session-sync rail — everything else works without it |
|
|
19
22
|
|
|
20
|
-
Until 8.x this gem was the monolith: it shipped `space`, `architect`, and `src`
|
|
21
|
-
binaries (and `architect space …` / `architect src …` forwarders) plus the
|
|
22
|
-
vendored `Space::Core` / `Space::Src` subtrees. The 9.0.0 split carved the
|
|
23
|
-
tools into their own gems; this gem is the loop only. If you're on 8.x, get
|
|
24
|
-
`space` from the **space-cadet** gem and `src` from **repo-tender** now — and
|
|
25
|
-
`gem uninstall space-architect` first: the 8.x gem's `space`/`src` executables
|
|
26
|
-
collide with the ones the new gems ship.
|
|
27
|
-
|
|
28
|
-
## What's a space? 🪐
|
|
29
|
-
|
|
30
|
-
A space is just a regular directory with a tiny YAML identity file and room for
|
|
31
|
-
everything a task needs:
|
|
32
|
-
|
|
33
|
-
```text
|
|
34
|
-
~/architect/spaces/20260531-name-of-space/
|
|
35
|
-
space.yaml # identity: id, title, status, repos, notes, tags
|
|
36
|
-
README.md
|
|
37
|
-
repos/ # cloned (or copy-on-write'd) repositories
|
|
38
|
-
notes/ # scratch, prompts, logs
|
|
39
|
-
architecture/ # iteration files: I<NN>-<name>.md + ARCHITECT.md index + BRIEF.md
|
|
40
|
-
build/ # lane worktrees + scratch (build/<id>-<lane>/)
|
|
41
|
-
tmp/ # workspace-local temp — use this instead of /tmp
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
Run an `architect` command from inside a space and it just works — the loop
|
|
45
|
-
walks up from `$PWD` until it finds the nearest `space.yaml`. No "current
|
|
46
|
-
space" state to get out of sync; where you *are* is the space you mean. 🧭
|
|
47
|
-
|
|
48
|
-
Create and manage spaces with the **space-cadet** gem:
|
|
49
|
-
|
|
50
|
-
```sh
|
|
51
|
-
space init # create XDG config + state files
|
|
52
|
-
space new "Name of Space" -r org/repo -r org/lib # blast off a new space 🚀
|
|
53
|
-
space list # see all your spaces
|
|
54
|
-
```
|
|
55
|
-
|
|
56
23
|
## Installation 📦
|
|
57
24
|
|
|
58
|
-
Add it to your `Gemfile`:
|
|
59
|
-
|
|
60
|
-
```ruby
|
|
61
|
-
gem "space-architect"
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
```bash
|
|
65
|
-
bundle install
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
Or grab it yourself:
|
|
69
|
-
|
|
70
25
|
```bash
|
|
71
26
|
gem install space-architect
|
|
72
27
|
gem install repo-tender # optional: enables the sessions launchd agent
|
|
73
28
|
```
|
|
74
29
|
|
|
75
|
-
The gem installs one executable: `architect`. 🎀
|
|
76
|
-
|
|
77
|
-
## Quick start 🎀
|
|
78
|
-
|
|
79
|
-
```sh
|
|
80
|
-
# Spaces (space-cadet gem)
|
|
81
|
-
space init # create XDG config + state files
|
|
82
|
-
space new "Name of Space" -r org/repo -r org/lib # blast off a new space 🚀
|
|
83
|
-
space use 20260531-name-of-space # cd into a space by id
|
|
84
|
-
|
|
85
|
-
# Architect Loop (run from inside a space)
|
|
86
|
-
architect install-skills # install agent skills (once per machine)
|
|
87
|
-
architect init # scaffold ARCHITECT.md + architecture/
|
|
88
|
-
architect new my-feature # scaffold the next iteration file
|
|
89
|
-
architect freeze my-feature # lock the Acceptance Criteria ❄️
|
|
90
|
-
architect dispatch my-feature lane-a # send a headless builder to work
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
## `architect` — the Architect Loop 🏗️
|
|
94
|
-
|
|
95
|
-
The **Architect Loop** is a structured build cycle for you and headless AI
|
|
96
|
-
builders. Each loop lives inside a space as a *project*.
|
|
97
|
-
|
|
98
|
-
**Roles:**
|
|
99
|
-
|
|
100
|
-
- **Architect** — the judgment role: a strong reasoning model (or you), run
|
|
101
|
-
interactively. Arbitrates disagreements, writes and freezes iteration files,
|
|
102
|
-
calls kill/continue, merges builder output. Never writes implementation code.
|
|
103
|
-
- **Builder** — the execution role: a cheaper model run headless via `architect
|
|
104
|
-
dispatch`, one per lane in its own git worktree. Reads the iteration's Builder
|
|
105
|
-
Prompt, does the work, writes raw evidence to `build/<id>-<lane>/report.md`.
|
|
106
|
-
Never grades its own work; never edits `architecture/`.
|
|
107
|
-
|
|
108
|
-
The loop is **model-agnostic** — which models fill the two roles is your choice
|
|
109
|
-
(e.g. a strong Claude model judging a cheaper one on the same plan, or a
|
|
110
|
-
cross-vendor pairing for more independent review). Set it per dispatch with
|
|
111
|
-
`architect dispatch --model …`, or run several pairings head-to-head as a
|
|
112
|
-
**variant set** (`architect variant add`). See
|
|
113
|
-
[docs/DESIGN.md](docs/DESIGN.md) §1–§2 for the reasoning.
|
|
114
|
-
|
|
115
|
-
**Filesystem layout:**
|
|
116
|
-
|
|
117
|
-
```text
|
|
118
|
-
architecture/
|
|
119
|
-
ARCHITECT.md # cross-iteration index; project-wide state
|
|
120
|
-
BRIEF.md # durable §-numbered project contract (optional)
|
|
121
|
-
I01-<iteration>.md # one self-contained file per iteration
|
|
122
|
-
build/
|
|
123
|
-
I01-<iteration>-<lane>/ # lane worktree + scratch per dispatch
|
|
124
|
-
run.jsonl # streamed builder output
|
|
125
|
-
report.md # builder report (transcribed into the iteration file verbatim)
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
**Iteration file anatomy** — one file, grown section by section. You author the
|
|
129
|
-
*content* in fresh scratch files; the CLI owns the *persistence* (each command
|
|
130
|
-
writes the section, commits it, and prints back what changed). Every committing
|
|
131
|
-
command takes `-m`/`--message` and `--message-from <file>` — your first line
|
|
132
|
-
completes the subject after a short canonical prefix (`I01 spec: <subject>`),
|
|
133
|
-
the rest becomes the body. The space's git log is the loop's durable memory:
|
|
134
|
-
write detailed messages.
|
|
135
|
-
|
|
136
|
-
| Section | Holds | How you persist it |
|
|
137
|
-
|---------|-------|--------------------|
|
|
138
|
-
| `## Grounds` | why — research / brief distilled (optional) | `architect section <it> grounds --from <f>` |
|
|
139
|
-
| `## Specification` | what/how — the full delegation contract | `architect section <it> specification --from <f>` |
|
|
140
|
-
| `## Acceptance Criteria` | proof — exact gate commands + thresholds | `architect freeze <it>` ❄️ |
|
|
141
|
-
| `## Builder Prompt` | the exact lane-prompt(s) dispatched | `architect section <it> prompt --append --lane <l> --from <f>` |
|
|
142
|
-
| `## Builder Report` | raw evidence, transcribed verbatim | `architect evidence <it> --lane <l>` |
|
|
143
|
-
| `## Verdict` | rulings + per-AC PASS/FAIL + KILL/CONTINUE | `architect section <it> verdict --from <f>` |
|
|
144
|
-
|
|
145
|
-
**The freeze ❄️** — `architect freeze <iteration>` commits the frozen region
|
|
146
|
-
(Grounds / Specification / Acceptance Criteria), records the `freeze_sha`, and
|
|
147
|
-
prints the frozen Acceptance Criteria back. Any change to those sections
|
|
148
|
-
afterward is an automatic iteration FAIL. The builder never edits the iteration
|
|
149
|
-
file.
|
|
150
|
-
|
|
151
|
-
**Re-grounding 🧭** — `architect init` also scaffolds a `SessionStart` hook that
|
|
152
|
-
runs `architect ground` (emitting `ARCHITECT.md`, `BRIEF.md`, and the in-flight
|
|
153
|
-
iteration) so every fresh session starts oriented — the loop leans on
|
|
154
|
-
fresh-session judgment, and this is what makes picking up cold cheap. Builders
|
|
155
|
-
inside a lane worktree are never grounded.
|
|
156
|
-
|
|
157
|
-
**Command surface:**
|
|
158
|
-
|
|
159
|
-
```sh
|
|
160
|
-
architect init # scaffold ARCHITECT.md + the space.yaml project: block + SessionStart hook
|
|
161
|
-
architect brief new --from <f> # write the durable project BRIEF.md (authored in a scratch file)
|
|
162
|
-
architect new <iteration> # scaffold architecture/I<NN>-<iteration>.md
|
|
163
|
-
architect section <it> <section> --from <f> # write + commit a section (add -m/--message-from for a detailed commit)
|
|
164
|
-
architect freeze <iteration> # freeze the Acceptance Criteria ❄️
|
|
165
|
-
architect worktree add <repo> <it> <lane> # isolated worktree per lane (2–4 lanes)
|
|
166
|
-
architect dispatch <it> <lane> --prompt <f> # copy the lane prompt in + dispatch a builder (--detach to survive long runs)
|
|
167
|
-
architect verify <iteration> # post-flight mechanical checks (reports only)
|
|
168
|
-
architect evidence <it> --lane <lane> # transcribe the builder's report verbatim
|
|
169
|
-
architect gate <iteration> # run the frozen gate commands, stream raw output
|
|
170
|
-
architect merge <it> <lane> # integrate ONE judged-passing lane (--no-ff)
|
|
171
|
-
architect integrate <it> --lanes a,b # integrate a set of passing lanes, in order
|
|
172
|
-
# landing (PR body + push/PR command) is the architect's end-of-project procedure (see skill)
|
|
173
|
-
architect status # project state (read-only)
|
|
174
|
-
architect variant add|compare|promote … # competing (harness, model) lanes over one frozen spec
|
|
175
|
-
architect research dispatch|status|wait … # parallel read-only research lanes (see below)
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
`architect help` (or bare `architect`, `architect --help`) lists these grouped by
|
|
179
|
-
loop phase — **Spec · Build · Judge · Land · Project · Groups**, in canonical
|
|
180
|
-
order — and, inside an architect space, ends with a compact loop-status block
|
|
181
|
-
(project status + current iteration) so you can see where you are in the loop. 🧭
|
|
182
|
-
|
|
183
|
-
A typical session:
|
|
184
|
-
|
|
185
|
-
```sh
|
|
186
|
-
architect init # first time
|
|
187
|
-
architect new my-feature # scaffold I01-my-feature.md
|
|
188
|
-
architect section my-feature specification --from spec.md -m "pull-based dispatcher seam"
|
|
189
|
-
architect freeze my-feature # lock it ❄️
|
|
190
|
-
architect dispatch my-feature lane-a --prompt tmp/prompts/I01-lane-a.md --detach # send a builder; poll the report
|
|
191
|
-
architect verify my-feature # mechanical post-flight checks
|
|
192
|
-
architect evidence my-feature --lane lane-a # transcribe raw evidence
|
|
193
|
-
architect gate my-feature # run the frozen gates yourself
|
|
194
|
-
# … read the diff against the spec, then write the Verdict …
|
|
195
|
-
architect integrate my-feature --lanes lane-a # merge passing lanes → project/<slug>
|
|
196
|
-
# landing (PR body + push/PR command) is the architect's end-of-project procedure (see skill)
|
|
197
|
-
```
|
|
198
|
-
|
|
199
|
-
### Streaming builder output 📡
|
|
200
|
-
|
|
201
|
-
`architect dispatch` can push the builder's pi JSONL event stream to an ingest
|
|
202
|
-
server for
|
|
203
|
-
live viewing:
|
|
204
|
-
|
|
205
|
-
```sh
|
|
206
|
-
# Push to an already-created run (you supply the full ingest URL):
|
|
207
|
-
architect dispatch my-feature lane-a \
|
|
208
|
-
--push-url $HOST/runs/<id>/ingest \
|
|
209
|
-
--push-token $INGEST_TOKEN
|
|
210
|
-
|
|
211
|
-
# Create a run and push in one step (requires --push-token = server's INGEST_TOKEN):
|
|
212
|
-
architect dispatch my-feature lane-a \
|
|
213
|
-
--push-host $HOST \
|
|
214
|
-
--push-token $INGEST_TOKEN
|
|
215
|
-
```
|
|
216
|
-
|
|
217
|
-
`--push-host` POSTs to `<HOST>/runs`, parses the new run id from the `201`
|
|
218
|
-
response, derives `<HOST>/runs/<id>/ingest`, and streams there; the created run
|
|
219
|
-
id and ingest URL are printed after dispatch starts. `--push-url` and
|
|
220
|
-
`--push-host` are mutually exclusive, both require `--push-token`, and neither
|
|
221
|
-
can be combined with `--detach` (the push tees the live pipe in-process).
|
|
30
|
+
The gem installs one executable: `architect`. 🎀 (Or add
|
|
31
|
+
`gem "space-architect"` to your Gemfile and `bundle install`.)
|
|
222
32
|
|
|
223
|
-
|
|
33
|
+
Upgrading from the 8.x monolith? Uninstall `space-architect` 8.x **first** —
|
|
34
|
+
its `space`/`src` executables collide with the ones the new gems ship — see
|
|
35
|
+
[the upgrade guide](docs/how-to/upgrade-from-the-8x-monolith.md).
|
|
224
36
|
|
|
225
|
-
|
|
226
|
-
**read-only** research lanes — detached `pi -p --mode json` researchers
|
|
227
|
-
(read-only by prompt contract; the supervisor-injected guard denies git writes)
|
|
228
|
-
that you supervise:
|
|
37
|
+
## 60-second start 🎀
|
|
229
38
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
architect research wait # tails each lane's run.jsonl; --level 1-4, --quiet, --thinking
|
|
233
|
-
architect research status # status of dispatched runs
|
|
234
|
-
```
|
|
235
|
-
|
|
236
|
-
Researchers gather; the architect verifies the load-bearing claims against
|
|
237
|
-
sources and writes the iteration's **Grounds** section.
|
|
238
|
-
|
|
239
|
-
### Skills 🧠
|
|
240
|
-
|
|
241
|
-
`architect install-skills` installs the bundled `architect`,
|
|
242
|
-
`architect-research`, and `architect-vocabulary` skills for your harness:
|
|
39
|
+
Run one full loop iteration in a scratch space — the
|
|
40
|
+
[tutorial](docs/tutorials/one-loop-iteration.md) walks it end to end:
|
|
243
41
|
|
|
244
42
|
```sh
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
architect
|
|
248
|
-
architect
|
|
43
|
+
space init # create the XDG config + state files (space-cadet gem)
|
|
44
|
+
space new "Docsmith Tutorial" # blast off a new space 🚀
|
|
45
|
+
cd ~/architect/spaces/<id>-docsmith-tutorial
|
|
46
|
+
architect init # scaffold ARCHITECT.md + the space.yaml project: block
|
|
249
47
|
```
|
|
250
48
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
### Sessions: the launchd rail 🛰️ (requires repo-tender)
|
|
255
|
-
|
|
256
|
-
`architect sessions sync` uploads new conversation transcripts (`~/.pi/agent/sessions`
|
|
257
|
-
and `~/.claude/projects`) to a space-server; `architect sessions agent
|
|
258
|
-
install|uninstall|status` manages the per-user launchd agent that runs the sync
|
|
259
|
-
on an interval. These commands drive **repo-tender**'s launchd agent — a *soft*
|
|
260
|
-
dependency, deliberately absent from the gemspec: without the gem, the sessions
|
|
261
|
-
agent commands print a short error naming the fix (`gem install repo-tender`)
|
|
262
|
-
and the rest of `architect` is unaffected.
|
|
263
|
-
|
|
264
|
-
## Configuration ⚙️
|
|
265
|
-
|
|
266
|
-
The spaces config the loop reads lives at `~/.config/space-cadet/config.yml`
|
|
267
|
-
(XDG-aware, owned by the space-cadet gem) — see `space config show`. The loop
|
|
268
|
-
itself has no config of its own: a space's `space.yaml` `project:` block *is*
|
|
269
|
-
the loop's state.
|
|
49
|
+
From there the loop is: `architect new` → `architect section …` → `architect
|
|
50
|
+
freeze` → `architect dispatch` → `architect gate` → `architect integrate`. The
|
|
51
|
+
tutorial runs every step on a scratch repo.
|
|
270
52
|
|
|
271
|
-
##
|
|
53
|
+
## The four quadrants 🧭
|
|
272
54
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
```ruby
|
|
281
|
-
require "space_core" # just spaces (space-cadet gem)
|
|
282
|
-
require "space_architect" # the loop (pulls in the space-cadet gem)
|
|
283
|
-
```
|
|
284
|
-
|
|
285
|
-
## Documentation 📖
|
|
286
|
-
|
|
287
|
-
- **[Command Reference](docs/reference.md)** — every command, flag, and behavior
|
|
288
|
-
- **[Design](docs/DESIGN.md)** — the source-backed rationale: the twelve invariant rules (R1–R12), the failure-mode → mitigation table, and why the loop is shaped this way
|
|
289
|
-
- **[Changelog](CHANGELOG.md)** — release history
|
|
55
|
+
| Quadrant | Read it when you… | Start here |
|
|
56
|
+
|----------|-------------------|------------|
|
|
57
|
+
| **Tutorials** — learning-oriented | are new and want a guided journey | [docs/tutorials](docs/tutorials/) |
|
|
58
|
+
| **How-to guides** — goal-oriented | have a specific task to do | [docs/how-to](docs/how-to/) |
|
|
59
|
+
| **Reference** — information-oriented | need a fact about a command, flag, or file | [docs/reference](docs/reference/) |
|
|
60
|
+
| **Explanation** — understanding-oriented | want the why behind the design | [docs/explanation](docs/explanation/) |
|
|
290
61
|
|
|
291
62
|
## Development 🛠️
|
|
292
63
|
|
|
@@ -297,15 +68,9 @@ bundle exec rake build # build the gem into pkg/
|
|
|
297
68
|
bundle exec rake install # build + install into your user gem home
|
|
298
69
|
```
|
|
299
70
|
|
|
300
|
-
## Contributing 💝
|
|
301
|
-
|
|
302
71
|
Bug reports and pull requests are welcome on GitHub at
|
|
303
|
-
[https://github.com/jetpks/space-architect](https://github.com/jetpks/space-architect)
|
|
304
|
-
|
|
305
|
-
## License 📄
|
|
306
|
-
|
|
307
|
-
Available as open source under the terms of the
|
|
308
|
-
[MIT License](https://opensource.org/licenses/MIT).
|
|
72
|
+
[https://github.com/jetpks/space-architect](https://github.com/jetpks/space-architect).
|
|
73
|
+
Available as open source under the [MIT License](LICENSE.txt).
|
|
309
74
|
|
|
310
75
|
---
|
|
311
76
|
|
|
@@ -127,38 +127,47 @@ module Space::Architect
|
|
|
127
127
|
end
|
|
128
128
|
|
|
129
129
|
# Allocate the next ordinal and scaffold architecture/I<NN>-<iteration>.md.
|
|
130
|
+
# Serialized across processes: ordinal allocation is a read-modify-write of
|
|
131
|
+
# space.yaml's project block, and two concurrent `architect new` processes
|
|
132
|
+
# would duplicate the ordinal and silently drop the loser's iterations
|
|
133
|
+
# entry (last writer wins). The metadata is saved by atomic replace, so the
|
|
134
|
+
# lock is a sibling file under tmp/ (gitignored, space-standard) — never
|
|
135
|
+
# space.yaml itself — and the metadata is re-loaded inside the lock because
|
|
136
|
+
# every process booted with its own pre-scaffold snapshot.
|
|
130
137
|
def new_iteration!(name, message: nil)
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
138
|
+
with_scaffold_lock do
|
|
139
|
+
block = space.data["project"] || {}
|
|
140
|
+
iterations = block["iterations"] || []
|
|
141
|
+
if iterations.any? { |s| s["name"] == name }
|
|
142
|
+
raise Space::Core::Error, "iteration '#{name}' already exists in space.yaml"
|
|
143
|
+
end
|
|
136
144
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
145
|
+
ordinal = (iterations.map { |s| s["ordinal"] || 0 }.max || 0) + 1
|
|
146
|
+
nn = format("%02d", ordinal)
|
|
147
|
+
rel = "architecture/I#{nn}-#{name}.md"
|
|
148
|
+
path = space.path.join(rel)
|
|
149
|
+
raise Space::Core::Error, "#{rel} already exists" if path.exist?
|
|
142
150
|
|
|
143
|
-
|
|
144
|
-
|
|
151
|
+
FileUtils.mkdir_p(path.dirname)
|
|
152
|
+
path.write(render_iteration(nn, name))
|
|
145
153
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
154
|
+
update_architect_block do |b|
|
|
155
|
+
b["current_iteration"] = name
|
|
156
|
+
list = b["iterations"] || []
|
|
157
|
+
list << {
|
|
158
|
+
"name" => name, "ordinal" => ordinal, "file" => rel,
|
|
159
|
+
"freeze_sha" => nil, "verdict" => "pending", "lanes" => []
|
|
160
|
+
}
|
|
161
|
+
b["iterations"] = list
|
|
162
|
+
b
|
|
163
|
+
end
|
|
156
164
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
165
|
+
git_run("-C", space.path.to_s, "add", rel, Space::Core::Space::METADATA_FILE)
|
|
166
|
+
git_run("-C", space.path.to_s, "commit", "-m",
|
|
167
|
+
compose_message("I#{nn} scaffold:", "I#{nn}: scaffold #{name}", message))
|
|
160
168
|
|
|
161
|
-
|
|
169
|
+
path
|
|
170
|
+
end
|
|
162
171
|
end
|
|
163
172
|
|
|
164
173
|
def status
|
|
@@ -701,8 +710,12 @@ module Space::Architect
|
|
|
701
710
|
def worktree_add(repo, iteration, lane, base: nil, base_explicit: !base.nil?, harness: nil, model: nil,
|
|
702
711
|
variant: false, effort: nil, touch: nil, force: false, err: $stderr)
|
|
703
712
|
model, suffix_level = Harness.parse_model_suffix(model)
|
|
704
|
-
harness, model = resolve_harness_model(harness, model)
|
|
705
|
-
|
|
713
|
+
harness, model, inform = resolve_harness_model(harness, model)
|
|
714
|
+
err.puts inform if inform
|
|
715
|
+
# The config layer sits above project.effort — architect.yaml is the
|
|
716
|
+
# dedicated routing layer; unconfigured spaces fall through unchanged.
|
|
717
|
+
resolved_level = effort || suffix_level ||
|
|
718
|
+
Routing.for_space(space.path)&.dig("builder", "thinking") || project_defaults["effort"]
|
|
706
719
|
Harness.validate_thinking_level!(resolved_level)
|
|
707
720
|
if effort.nil? && suffix_level
|
|
708
721
|
err.puts "thinking: model suffix ':#{suffix_level}' parsed from lane model → level=#{suffix_level} (model stripped)"
|
|
@@ -1033,6 +1046,25 @@ module Space::Architect
|
|
|
1033
1046
|
|
|
1034
1047
|
attr_reader :space
|
|
1035
1048
|
|
|
1049
|
+
# Exclusive cross-process lock around one whole critical section (used by
|
|
1050
|
+
# new_iteration!): flock on .git/architect.lock — the metadata file itself
|
|
1051
|
+
# cannot be the lock because save() replaces it by atomic rename (a new
|
|
1052
|
+
# inode would not contend), and .git is the one directory every scaffoldable
|
|
1053
|
+
# space guarantees (new_iteration! commits) that git never reports as
|
|
1054
|
+
# untracked noise (git's own index.lock lives by the same convention).
|
|
1055
|
+
# Inside the lock the metadata is adopted in place (data.replace, keeping
|
|
1056
|
+
# object identity for every holder of this space) — each process booted
|
|
1057
|
+
# with its own pre-scaffold snapshot, so the ordinal must be read from
|
|
1058
|
+
# disk truth.
|
|
1059
|
+
def with_scaffold_lock
|
|
1060
|
+
lock_path = space.path.join(".git", "architect.lock")
|
|
1061
|
+
File.open(lock_path, File::RDWR | File::CREAT, 0o644) do |lock|
|
|
1062
|
+
lock.flock(File::LOCK_EX)
|
|
1063
|
+
space.data.replace(Space::Core::Space.load(space.path).data)
|
|
1064
|
+
yield
|
|
1065
|
+
end
|
|
1066
|
+
end
|
|
1067
|
+
|
|
1036
1068
|
# The vendored builder-guard extension shipped in the gem (lib/space_architect/pi/).
|
|
1037
1069
|
def vendored_guard_path
|
|
1038
1070
|
Pathname.new(__dir__).join("pi/builder-guard.ts")
|
|
@@ -1605,19 +1637,35 @@ module Space::Architect
|
|
|
1605
1637
|
end
|
|
1606
1638
|
|
|
1607
1639
|
# Resolve harness + model by precedence: explicit value > (dispatch only) the
|
|
1608
|
-
# lane's stored value >
|
|
1609
|
-
# default model. Shared by worktree_add
|
|
1610
|
-
# defaults. A stored model is only honored
|
|
1611
|
-
# the resolved harness — a dispatch-time
|
|
1612
|
-
#
|
|
1613
|
-
# legacy harness name survives resolution
|
|
1614
|
-
# surfaces as Harness.for's actionable pi-only error,
|
|
1640
|
+
# lane's stored value > architect.yaml builder-role model > space.yaml
|
|
1641
|
+
# project.harness/project.model > the pi default model. Shared by worktree_add
|
|
1642
|
+
# and dispatch so both read the same defaults. A stored model is only honored
|
|
1643
|
+
# when its stored harness still matches the resolved harness — a dispatch-time
|
|
1644
|
+
# harness override drops the old harness's stored model instead of leaking it
|
|
1645
|
+
# into the new harness's run. A stored legacy harness name survives resolution
|
|
1646
|
+
# (so a stale space.yaml surfaces as Harness.for's actionable pi-only error,
|
|
1647
|
+
# not a silent default).
|
|
1648
|
+
#
|
|
1649
|
+
# Returns [harness, model, inform] — inform is the loud last-resort line (nil
|
|
1650
|
+
# otherwise) for the caller to print once to its err stream when the model
|
|
1651
|
+
# fell all the way through to Harness::DEFAULT_MODEL (no flag, no stored
|
|
1652
|
+
# value, no architect.yaml builder role, no project.model).
|
|
1615
1653
|
def resolve_harness_model(harness, model, stored_harness: nil, stored_model: nil)
|
|
1616
1654
|
defaults = project_defaults
|
|
1617
1655
|
resolved_harness = (harness || stored_harness || defaults["harness"] || "pi").to_s
|
|
1618
1656
|
stored_model = nil unless stored_harness.nil? || stored_harness.to_s == resolved_harness
|
|
1619
|
-
|
|
1620
|
-
[
|
|
1657
|
+
config_model = Routing.for_space(space.path)&.dig("builder", "model")
|
|
1658
|
+
resolved_model = model || stored_model || config_model || defaults["model"] || Harness::DEFAULT_MODEL
|
|
1659
|
+
inform = default_model_inform if model.nil? && stored_model.nil? && config_model.nil? && defaults["model"].nil?
|
|
1660
|
+
[resolved_harness, resolved_model, inform]
|
|
1661
|
+
end
|
|
1662
|
+
|
|
1663
|
+
# The loud last-resort inform — the bare DEFAULT_MODEL constant is a last
|
|
1664
|
+
# resort, not a silent fallback: name that no architect.yaml was found, the
|
|
1665
|
+
# two paths looked at, and the default in use.
|
|
1666
|
+
def default_model_inform
|
|
1667
|
+
"model: no architect.yaml found (looked at #{Routing.space_path(space.path)} + #{Routing.global_path}) — " \
|
|
1668
|
+
"using default '#{Harness::DEFAULT_MODEL}'"
|
|
1621
1669
|
end
|
|
1622
1670
|
|
|
1623
1671
|
# --prompt: the caller authors the lane prompt anywhere (a fresh scratch file) and
|
|
@@ -1640,8 +1688,9 @@ module Space::Architect
|
|
|
1640
1688
|
# precedence and thinking-level translation from one place.
|
|
1641
1689
|
def resolve_dispatch_harness(lane_entry, model:, harness:, effort:, force:, err:)
|
|
1642
1690
|
model, suffix_level = Harness.parse_model_suffix(model)
|
|
1643
|
-
resolved_harness, resolved_model = resolve_harness_model(harness, model,
|
|
1691
|
+
resolved_harness, resolved_model, inform = resolve_harness_model(harness, model,
|
|
1644
1692
|
stored_harness: lane_entry["harness"], stored_model: lane_entry["model"])
|
|
1693
|
+
err.puts inform if inform
|
|
1645
1694
|
|
|
1646
1695
|
resolved_effort =
|
|
1647
1696
|
if effort
|
|
@@ -1651,7 +1700,9 @@ module Space::Architect
|
|
|
1651
1700
|
err.puts "thinking: model suffix ':#{suffix_level}' parsed from --model → level=#{suffix_level} (model stripped to '#{model}')"
|
|
1652
1701
|
suffix_level
|
|
1653
1702
|
else
|
|
1654
|
-
|
|
1703
|
+
# Dispatch's tail is the lane's stored effort (project.effort was never
|
|
1704
|
+
# in the dispatch chain); architect.yaml builder thinking slots in above it.
|
|
1705
|
+
lane_entry["effort"] || Routing.for_space(space.path)&.dig("builder", "thinking")
|
|
1655
1706
|
end
|
|
1656
1707
|
|
|
1657
1708
|
[resolved_harness, resolved_model, resolved_effort]
|