space-architect 5.5.1 → 6.0.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.
@@ -53,7 +53,7 @@ module Space::Core
53
53
  def list
54
54
  return [] unless spaces_dir.directory?
55
55
 
56
- spaces_dir.children.select(&:directory?).filter_map do |child|
56
+ Paths.layout_children(spaces_dir).select(&:directory?).filter_map do |child|
57
57
  Space.load(child)
58
58
  rescue NotFoundError, Error
59
59
  nil
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Space
4
4
  module Core
5
- VERSION = "5.5.1"
5
+ VERSION = "6.0.0"
6
6
  end
7
7
  end
@@ -43,6 +43,19 @@ module Space::Src
43
43
  # the next spawn. No-op when the log is missing or
44
44
  # under-threshold (sync tests in G4 stay green).
45
45
  rotate_plist_logs(paths)
46
+
47
+ # Resolved BEFORE the scoping block: the "scoping sync to:"
48
+ # notice below is prose, and must be suppressed in JSON mode
49
+ # for the same reason the summary line is. Resolution depends
50
+ # only on flags/env/out, so it is safe this early.
51
+ mode = UI::Mode.resolve(
52
+ flags: {plain: plain, json: json, no_color: no_color, quiet: quiet},
53
+ env: CLI.env,
54
+ out: out
55
+ )
56
+ # Every line of --json output must parse as one JSON object.
57
+ human = mode.format != :json
58
+
46
59
  if repo
47
60
  target = scope_target(repo)
48
61
  return fail_with(self, "invalid repo reference: #{repo.inspect} (expected host/owner/name)") if target.failure?
@@ -57,14 +70,9 @@ module Space::Src
57
70
  # exactly the G4 "other repo gets no state row" test
58
71
  # path, so we explicitly empty orgs here).
59
72
  config = Config::Store.with(config, repos: [found], orgs: [])
60
- out.puts "scoping sync to: #{Repo::Helpers.format_ref(found)}"
73
+ out.puts "scoping sync to: #{Repo::Helpers.format_ref(found)}" if human
61
74
  end
62
75
 
63
- mode = UI::Mode.resolve(
64
- flags: {plain: plain, json: json, no_color: no_color, quiet: quiet},
65
- env: CLI.env,
66
- out: out
67
- )
68
76
  reporter = if mode.format == :json
69
77
  UI::JsonReporter.new(out)
70
78
  elsif mode.animate
@@ -73,14 +81,19 @@ module Space::Src
73
81
  UI::PlainReporter.new(out, mode: mode)
74
82
  end
75
83
 
76
- result = Space::Src::Sync::Engine.new(reporter: reporter).call(config: config, paths: paths)
84
+ # prune only on a full sweep. With --repo the config above was
85
+ # narrowed to one repo, so pruning would delete the state rows
86
+ # for every OTHER tracked repo.
87
+ result = Space::Src::Sync::Engine.new(reporter: reporter)
88
+ .call(config: config, paths: paths, prune: repo.nil?)
77
89
  if result.failure?
78
90
  return fail_with(self, "sync failed: #{format_failure(result.failure)}")
79
91
  end
80
92
 
81
- new_state = result.success
82
- n = new_state.repos.size
83
- out.puts "synced #{n} repo(s)"
93
+ report = result.success
94
+ # JsonReporter already emits a run_finished event carrying the
95
+ # summary, so suppressing the prose line loses nothing.
96
+ out.puts "synced #{report.processed} repo(s)" if human
84
97
  CLI.record_outcome(Outcome.new(exit_code: 0))
85
98
  end
86
99
 
data/lib/space_src/cli.rb CHANGED
@@ -69,6 +69,7 @@ module Space::Src
69
69
  def self.bare_query?(argv)
70
70
  return false if TOP_LEVEL_HELP.include?(argv)
71
71
  return false if VERSION_REQUEST.include?(argv)
72
+ # paths:exempt - Registry.get([]).children is a dry-cli command-tree node, not a filesystem path
72
73
  argv.length == 1 && !Registry.get([]).children.key?(argv[0])
73
74
  end
74
75
 
@@ -37,6 +37,7 @@ module Space::Src
37
37
  parts = name.split("/")
38
38
  case parts.length
39
39
  when 1
40
+ # paths:exempt - layout intent (checkout resolution by depth); space_src is deliberately independent of space_core and must not require it
40
41
  candidates = Dir.glob(File.join(@base_dir, "*", "*", name))
41
42
  case candidates.length
42
43
  when 0 then Failure("#{name.inspect} not found under base_dir #{@base_dir}")
@@ -45,6 +46,7 @@ module Space::Src
45
46
  end
46
47
  when 2
47
48
  owner, repo_name = parts
49
+ # paths:exempt - layout intent (checkout resolution by depth); space_src is deliberately independent of space_core and must not require it
48
50
  candidates = Dir.glob(File.join(@base_dir, "*", owner, repo_name))
49
51
  case candidates.length
50
52
  when 0 then Failure("#{name.inspect} not found under base_dir #{@base_dir}")
data/lib/space_src/nav.rb CHANGED
@@ -16,6 +16,7 @@ module Space::Src
16
16
  def self.scan(base_dir)
17
17
  pattern = File.join(base_dir, "*", "*", "*")
18
18
  prefix = base_dir.chomp("/") + "/"
19
+ # paths:exempt - layout intent (depth-3 checkout scan); space_src is deliberately independent of space_core and must not require it
19
20
  Dir.glob(pattern).filter_map do |path|
20
21
  next unless File.directory?(path)
21
22
  relative = path.delete_prefix(prefix)
@@ -13,6 +13,7 @@ require "space_src/state/store"
13
13
  require "space_src/state/lock"
14
14
  require "space_src/paths"
15
15
  require "space_src/sync/repo_plan"
16
+ require "space_src/sync/report"
16
17
  require "space_src/ui/reporter"
17
18
 
18
19
  module Space::Src
@@ -79,8 +80,18 @@ module Space::Src
79
80
  # Runs one sync pass.
80
81
  # @param config [Config::Config] the validated config struct
81
82
  # @param paths [Paths] the XDG paths object
82
- # @return [Dry::Monads::Result<State::Store::State>]
83
- def call(config:, paths:)
83
+ # @param prune [Boolean] drop state rows for repos this run did
84
+ # not see. Only the CALLER can know whether `config` is the
85
+ # complete tracked set or a scoped subset — the engine cannot
86
+ # tell "user tracks one repo" from "user passed --repo" — so
87
+ # this is never inferred here. Defaults to false: never delete
88
+ # unless explicitly told to. Even when true, pruning is skipped
89
+ # if any org listing failed (see below).
90
+ # @return [Dry::Monads::Result<Report>] the merged state plus the
91
+ # count of repos this run processed. The two are distinct: state
92
+ # accumulates every repo ever synced, `processed` is this run's
93
+ # work (see Sync::Report).
94
+ def call(config:, paths:, prune: false)
84
95
  Sync do |task|
85
96
  semaphore = Async::Semaphore.new(config.concurrency, parent: task)
86
97
  barrier = Async::Barrier.new
@@ -147,12 +158,24 @@ module Space::Src
147
158
  @reporter.run_finished(summary)
148
159
 
149
160
  # Phase 4: assemble new state, write once.
150
- new_state = build_new_state(state, results, org_records)
161
+ #
162
+ # A failed org listing means `discovered` is missing that
163
+ # org's repos through no fault of the user's config, so
164
+ # pruning would delete every row for a whole org on a
165
+ # transient forge outage. Only prune when the run saw the
166
+ # complete picture.
167
+ listings_complete = org_records.each_value.none?(&:last_error)
168
+ new_state = build_new_state(
169
+ state, results, org_records, prune: prune && listings_complete
170
+ )
151
171
  write_result = State::Store.write(paths.state_file, new_state)
152
172
  if write_result.failure?
153
173
  write_result
154
174
  else
155
- Dry::Monads::Success(new_state)
175
+ # results.size, not new_state.repos.size — the latter is
176
+ # the accumulated state file and would report every repo
177
+ # ever synced (553 on a scoped one-repo run).
178
+ Dry::Monads::Success(Report.new(state: new_state, processed: results.size))
156
179
  end
157
180
  ensure
158
181
  @reporter.detach
@@ -161,7 +184,11 @@ module Space::Src
161
184
 
162
185
  if lock_result == State::Lock::NOT_ACQUIRED
163
186
  warn "src: skipped — another sync in progress"
164
- Dry::Monads::Success(State::Store.load(paths.state_file).success)
187
+ # Nothing ran, so processed is 0 — the state is returned only
188
+ # so callers still get a usable snapshot.
189
+ Dry::Monads::Success(
190
+ Report.new(state: State::Store.load(paths.state_file).success, processed: 0)
191
+ )
165
192
  else
166
193
  lock_result
167
194
  end
@@ -445,8 +472,20 @@ module Space::Src
445
472
  # run's per-repo outcomes + the org records. Failures get a
446
473
  # status: error row so every processed repo has a state entry
447
474
  # (gate G8).
448
- def build_new_state(prev, results, org_records)
449
- repos = prev.repos.dup
475
+ #
476
+ # With prune: false (the default) rows for repos this run did not
477
+ # process are carried over untouched — that is what keeps a scoped
478
+ # `sync --repo` from deleting every other repo's state.
479
+ #
480
+ # With prune: true the carry-over is dropped, so the new state
481
+ # holds exactly the repos this run processed and rows for repos
482
+ # removed from config stop accumulating. Starting from an empty
483
+ # hash is equivalent to selecting the processed keys out of prev:
484
+ # every repo in the sweep contributes exactly one result tuple
485
+ # (process_one's rescue guarantees one even on an unhandled
486
+ # raise), so each gets a fresh row in the merge below.
487
+ def build_new_state(prev, results, org_records, prune: false)
488
+ repos = prune ? {} : prev.repos.dup
450
489
  results.each do |key, repo, error|
451
490
  repos[key] = (repo || State::Store::Repo.new(
452
491
  default_branch: nil,
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Space::Src
4
+ module Sync
5
+ # What one sync pass did.
6
+ #
7
+ # `state` is the merged whole-state that gets written to disk: it
8
+ # carries a row for every repo ever synced, because build_new_state
9
+ # starts from the previous state and merges this run's outcomes into
10
+ # it. `processed` counts only the repos this run actually touched.
11
+ #
12
+ # The two diverge whenever the run is scoped (`sync --repo`) or the
13
+ # state file already holds rows from earlier runs — which is why
14
+ # callers wanting "how many repos did this run handle?" must read
15
+ # `processed` and never `state.repos.size`.
16
+ Report = Data.define(:state, :processed)
17
+ end
18
+ end
data/lib/space_src.rb CHANGED
@@ -25,6 +25,7 @@ require "space_src/scm/git"
25
25
  require "space_src/forge/client"
26
26
  require "space_src/forge/github"
27
27
  require "space_src/sync/repo_plan"
28
+ require "space_src/sync/report"
28
29
  require "space_src/sync/engine"
29
30
  require "space_src/config/duration"
30
31
  require "space_src/log_rotator"
@@ -170,8 +170,20 @@ re-prints them, or `git show <freeze-sha>:architecture/I<NN>-<name>.md`). For ea
170
170
  gate: run the gate command yourself — `architect gate <iteration>` runs the
171
171
  frozen gate commands in the resolved repo/worktree and streams raw output (it is a
172
172
  runner, never a judge), or run them by hand — then compare the output against the
173
- verbatim frozen text → **PASS / FAIL / INVALID** (INVALID = not measured the way
174
- the gate specifies). Check `git diff <freeze-sha> HEAD --
173
+ verbatim frozen text → **PASS / FAIL / INVALID**. The line between the failing
174
+ two is the mechanism. **FAIL**: the instrument measured exactly what the gate
175
+ says, and the answer was wrong. **INVALID**: the instrument did not measure what
176
+ the prose AC asserts — a quoting error, a `2>&1` capturing unrelated stderr, a
177
+ command that dies in the runner's shell. INVALID is a legitimate, expected
178
+ outcome, not a loop failure: prove the property directly with evidence gathered
179
+ this session, and carry the corrected gate into the next iteration's freeze
180
+ rather than leaving the defect to recur. It is never a loosening route — a
181
+ threshold the results simply missed is FAIL, however near. A lane that raised a
182
+ **letter-versus-spirit conflict** (per the lane-prompt clause: it built the AC's
183
+ stated property rather than degrade the artifact to fit its letter, and reported
184
+ the conflict with evidence) has done its job — the defect is the criterion's
185
+ authoring (§4), and the lane is not penalized for it; rule on the conflict like
186
+ any disagreement. Check `git diff <freeze-sha> HEAD --
175
187
  architecture/I<NN>-<name>.md` — any change to Grounds/Specification/Acceptance
176
188
  Criteria lines is an automatic FAIL.
177
189
  Gate-pass is necessary, not sufficient: read the diff against the
@@ -267,10 +279,21 @@ contract, self-contained:
267
279
  declaration lives in a fenced ` ```lanes ` block in the Specification — one
268
280
  entry per lane (`name`, `repo`, `touch` globs) — the single frozen source of
269
281
  truth `architect freeze` records into `space.yaml` and `architect provision`
270
- materializes. The touch-set now lives *with* the frozen spec by design: it
271
- closes the drift where a `worktree add --touch` flag could diverge from the
272
- spec's intent. The scaffold ships a commented ` ```lanes ` stub in the
273
- Specification (see `templates/iteration.md.erb`) uncomment it. Lanes in
282
+ materializes. The same boundary is stated twice more the lane-prompt's
283
+ may-touch list, and the scope gate that checks the lane's diff at judge
284
+ time and all three must be one exhaustive enumeration: every file listed,
285
+ no glob standing in for the list; the scope gate's pattern is that
286
+ enumeration exactly; the prompt's list is generated from it. Consistency is
287
+ not completeness — three statements drawn from one too-narrow search agree
288
+ and are wrong together — so before freezing a criterion that requires an
289
+ identifier to change name, grep the whole repo for it and confirm every hit
290
+ falls inside some lane's touch set; `architect rehearse` reports this
291
+ asymmetry, so the check is read, not remembered. Either defect surfaces
292
+ only after the freeze, where hard rule 3 forbids widening the declaration:
293
+ `integrate` refuses a correct lane, or the stray hit breaks a file no lane
294
+ may legally fix. The scaffold ships a
295
+ commented ` ```lanes ` stub in the Specification (see
296
+ `templates/iteration.md.erb`) — uncomment it. Lanes in
274
297
  *different* repos are inherently disjoint; same-repo lanes with any file
275
298
  overlap run as one. Each lane gets its own objective, output format, and
276
299
  boundaries. Most
@@ -281,10 +304,11 @@ contract, self-contained:
281
304
  **serial deferred judgment** (iterations run to gates-green with `architect
282
305
  verdict` withheld; one later batch session judges each against its own frozen
283
306
  AC — see `### Serial deferred judgment`).
284
- - **Effort call** — thinking budget set in the lane-prompt via the escalation
285
- keywords (`think hard` `ultrathink`); default unattended builder work high,
286
- downgrade a routine, tightly-specified lane (record which and why). Claude
287
- Code has no per-invocation effort flag see `dispatch.md`.
307
+ - **Effort call** — thinking budget set per dispatch with `architect dispatch
308
+ --effort <level>`, translated and clamped to the lane's harness (the
309
+ escalation keywords `think hard` `ultrathink` still work in-prompt);
310
+ default unattended builder work high, downgrade a routine, tightly-specified
311
+ lane (record which and why). Levels and mechanics: `dispatch.md`.
288
312
 
289
313
  **Spike (probe) iterations.** When the open question is too uncertain for a
290
314
  build — the repo can't answer it and routine API-verification won't resolve it
@@ -307,7 +331,61 @@ through the normal builder/lane machinery.
307
331
  Then write the **Acceptance Criteria** section — prose conditions (AC1, AC2, …)
308
332
  that the architect judges against, followed by a fenced ` ```gates ` block of
309
333
  runnable checks (each gate carries `id`, `ac`, `cmd`, and `expect`; `cwd` is
310
- optional) and run `architect freeze <name>`. What must be frozen before
334
+ optional). Calibrate each criterion's precision to the property it asserts
335
+ R4 ("grade the outcome, not the path") applied at authoring time, the only time
336
+ it can be applied, because a frozen criterion is never loosened after results.
337
+ Two kinds of criterion, two calibrations:
338
+
339
+ - A criterion asserting a **property that must hold** is bounded from one side
340
+ only — the side the defect is on — freezing a floor, not an equality, so an
341
+ addition or change the lane discovers stays legal. "Exactly eight runtime
342
+ dependencies" freezes today's snapshot and outlaws the ninth a transitive
343
+ need later forces; the property it stood in for was "declares everything it
344
+ requires."
345
+ - A criterion whose **number is itself the deliverable** may be exact — there
346
+ the precision is the point (a touch-set boundary, a wire-format constant).
347
+
348
+ Writing the first kind as the second is the defect: it leaves the lane no
349
+ sanctioned path except degrading the artifact to fit the number.
350
+
351
+ Say, too, what each gate proves: a presence-grep gate on prose is a
352
+ **tripwire**, never the proof — it shows a keyword landed, not that the thing
353
+ works — and its criterion should say which it is, leaving the substance
354
+ architect-read.
355
+
356
+ **Pre-freeze check** — run against the drafted AC in the minutes before
357
+ `architect freeze`, while a bad criterion still costs nothing:
358
+
359
+ - **snapshot** — no AC or gate hard-codes a count, name-set, or byte-identity
360
+ that is merely what was known at freeze time;
361
+ - **control** — every baseline or regression claim had its control actually run
362
+ in this session, not recalled or assumed;
363
+ - **mechanism** — no AC names a *how* (an API signature, a helper, a file
364
+ layout) where the *what* is the requirement, such that a better shape the
365
+ lane discovers would be forbidden;
366
+ - **interface** — any CLI surface a Specification names — a verb, a flag
367
+ spelling, a subcommand — is executed once before it is frozen: a frozen
368
+ interface is only safe if it is implementable, which is the assumption
369
+ every code/prose lane split rests on. No verb backs this item yet — unlike
370
+ dry-run, it is a discipline run by hand;
371
+ - **dry-run** — `architect rehearse <name>`: it runs the drafted gates from
372
+ the working tree through the same execution path `architect gate` uses
373
+ (`/bin/sh`, same run-dir resolution, same evaluator) and classifies each —
374
+ **RED**, a clean non-zero: the gate discriminates, the healthy pre-freeze
375
+ result; **GREEN**, passes on base: a declared regression guard, or a gate
376
+ that measures nothing; **BROKEN**, a 127 / syntax error / timeout —
377
+ advisory, because a correct RED can look broken (`grep -q x` on a file the
378
+ lane will write exits 2): the tool names the suspicion, you confirm;
379
+ **EMPTY**, no gates or an untouched placeholder. `--record` emits a
380
+ paste-able provenance block for the AC preamble. It reports; which GREENs
381
+ are guards and which are defects stays your call.
382
+
383
+ Then run `architect freeze <name>`. Freeze requires a fresh rehearsal stamp —
384
+ `rehearse` records one in `space.yaml`, keyed to the gates block's content, so
385
+ editing a gate afterward stales it; `architect freeze --skip-rehearse REASON`
386
+ (non-empty, recorded) is the escape valve. The stamp records that you looked,
387
+ never that gates passed — an all-RED and an all-GREEN run stamp identically.
388
+ What must be frozen before
311
389
  dispatch is the Acceptance Criteria: `architect freeze` lints the gates block
312
390
  (absent or empty gates is allowed but warns; malformed fails), commits any
313
391
  pending content in the frozen region (Grounds/Specification/Acceptance Criteria)
@@ -319,12 +397,16 @@ once a frozen section changed afterward.
319
397
 
320
398
  ### 5. Dispatch (one fresh `claude -p` per lane, worktree-isolated)
321
399
 
322
- Per the mechanics in `dispatch.md`. The lane lifecycle is **declare → freeze →
323
- provision → write prompts → dispatch** — every lane gets a worktree; there is no
324
- dispatch-in-the-checkout path:
400
+ Per the mechanics in `dispatch.md`. The lane lifecycle is **declare → rehearse
401
+ → freeze → provision → write prompts → dispatch** — every lane gets a worktree;
402
+ there is no dispatch-in-the-checkout path:
325
403
 
326
404
  - **Declare** — at spec time, each lane is one entry in the Specification's
327
405
  fenced ` ```lanes ` block (§4): `name`, `repo`, `touch` globs.
406
+ - **Rehearse** — `architect rehearse <iteration>` dry-runs the drafted gates
407
+ from the working tree (§4's pre-freeze check), resolving its run dir from the
408
+ drafted ` ```lanes ` block, and stamps `space.yaml`; `freeze` refuses without
409
+ a fresh stamp.
328
410
  - **Freeze** — `architect freeze` parses that block and records each lane
329
411
  (name, repo, touch_set) into `space.yaml`.
330
412
  - **Provision** — `architect provision <iteration>` materializes every declared
@@ -223,7 +223,7 @@ AC judgment into one later session.
223
223
 
224
224
  **Recipe:**
225
225
 
226
- Per iteration, freeze and dispatch as normal. In a fresh judging session, run
226
+ Per iteration, rehearse, freeze, and dispatch as normal. In a fresh judging session, run
227
227
  post-flight, integrate, and gate — but **withhold `architect verdict`**:
228
228
 
229
229
  ```bash
@@ -273,13 +273,15 @@ compound it.
273
273
  <builder-model>`). A floating alias (a bare "latest"/tier tag) drifts to
274
274
  whatever ships next — fine interactively, but automations pin the full id so a
275
275
  model bump can't silently change builder behavior mid-project.
276
- - Effort = thinking budget. Claude Code has no per-invocation effort flag the
277
- way Codex exposed `model_reasoning_effort`; the builder sets thinking depth
278
- **in the block** via the escalation keywords (`think` < `think hard` <
279
- `think harder` < `ultrathink`), or you floor it with the `MAX_THINKING_TOKENS`
280
- env var on the dispatch. Default unattended builder work to a high budget
281
- (open the block with "Think harder…"); downgrade a routine,
282
- tightly-specified lane to "think hard" (record which and why in the spec).
276
+ - Effort = thinking budget. Set it per dispatch: `architect dispatch --effort
277
+ <level>` (aliases `--thinking`/`--reasoning`) accepts
278
+ `off`/`minimal`/`low`/`medium`/`high`/`xhigh`/`max` and translates it to the
279
+ lane's harness `claude-code` passes `low`…`max` straight through to its own
280
+ `--effort` flag, unclamped (`minimal` clamps to `low`; `off` omits the flag).
281
+ The escalation keywords (`think` < `think hard` < `think harder` <
282
+ `ultrathink`) and the `MAX_THINKING_TOKENS` env var still raise depth from
283
+ inside the block. Default unattended builder work to a high budget; downgrade
284
+ a routine, tightly-specified lane (record which and why in the spec).
283
285
  - **Builders never commit, and the architect verifies it.** Claude Code has no
284
286
  sandbox to make `.git` read-only, so this is enforced by the deny rules at
285
287
  dispatch *and* checked after the run: before integrating a lane, confirm
@@ -340,7 +342,10 @@ builders toward the repo's existing test fixtures over hand-rolled long-running
340
342
  harnesses, and when a gate needs a runtime that can't run unattended
341
343
  (interactive prompts, servers without a timeout), have the builder record the
342
344
  exact failure as a disagreement/blocker and verify what it can — gate verdicts
343
- are architect-run anyway (hard rule 4). Write the gate file anticipating this.
345
+ are architect-run anyway (hard rule 4). Write the gate file anticipating this
346
+ `architect rehearse` runs the drafted gates pre-freeze, so an unattended-hostile
347
+ gate surfaces as **BROKEN** while still editable (advisory: a correct RED can
348
+ look broken).
344
349
 
345
350
  ## Manual alternative (human-driven)
346
351
 
@@ -365,7 +370,11 @@ PHASE 1 — Treat the shared contracts (schemas/interfaces) named in the spec,
365
370
  and the repo's existing public interfaces, as FROZEN: do not change them —
366
371
  other lanes depend on them. You have no access to the space's architecture/
367
372
  directory; the architect owns it. The ACCEPTANCE CRITERIA below are frozen —
368
- verify your work against them; never weaken or work around them.
373
+ verify your work against them; never weaken or work around them. If an AC's
374
+ letter can only be satisfied by making the artifact worse than the property the
375
+ AC asserts, build the right thing and raise the conflict in your report — name
376
+ the AC, the conflict, and the evidence. This is not permission to skip work or
377
+ weaken a criterion: the conflict is reported, never silently resolved.
369
378
 
370
379
  PHASE 2 — Build YOUR LANE ONLY: exactly the files listed in BOUNDARIES. You
371
380
  are one of several parallel lane agents working in isolated worktrees; files
@@ -384,14 +393,20 @@ running the acceptance criteria's gate commands and record the verbatim output.
384
393
  git write command (commit/add/branch/reset/checkout) — the architect commits
385
394
  and merges after verification, and verifies you made no commits. Do NOT delete
386
395
  lock files or escalate privileges if a command fails; record the exact error
387
- and continue. Give every potentially long command an explicit timeout; if a
396
+ and continue. Do NOT use `run_in_background` (or shell `&`) for your own work —
397
+ this process terminates when you end your turn and reaps its own children, so
398
+ backgrounded work is SIGTERMed and its output lost while the run still exits
399
+ 0; run long commands serially in the foreground instead. Give every
400
+ potentially long command an explicit timeout; if a
388
401
  runtime will not start unattended (interactive prompt, server with no timeout),
389
402
  record the exact failure in your report and route around it — never busy-wait
390
403
  or retry in a loop. When done, write your report to the scratch file given to
391
404
  you, build/<id>-<lane>/report.md (an absolute path outside your worktree),
392
405
  with RAW results only — tables, numbers, command output — no interpretation, no
393
406
  "promising". Every status claim must be backed by a command result from this
394
- run. Keep the report compact — tables and numbers, not prose. End it with
407
+ run. Keep the report compact — tables and numbers, not prose. Do not title the
408
+ report — the architect's tooling supplies the `### <lane>` heading when it
409
+ transcribes; keep any headings inside the report at `###` or deeper. End it with
395
410
  exactly one status line: STATUS: COMPLETE | COMPLETE_WITH_CONCERNS (list them)
396
411
  | BLOCKED (exact blocker + what you tried). Verdicts belong to the architect
397
412
  and the human. Persist until your lane is fully handled end-to-end; do not stop
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: space-architect
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.5.1
4
+ version: 6.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Eric Jacobs
@@ -361,6 +361,7 @@ files:
361
361
  - lib/space_src/state/store.rb
362
362
  - lib/space_src/sync/engine.rb
363
363
  - lib/space_src/sync/repo_plan.rb
364
+ - lib/space_src/sync/report.rb
364
365
  - lib/space_src/ui/interactive_reporter.rb
365
366
  - lib/space_src/ui/json_reporter.rb
366
367
  - lib/space_src/ui/mode.rb