space-architect 5.5.0 → 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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +126 -0
- data/lib/space_architect/architect_project.rb +542 -56
- data/lib/space_architect/bug_report.rb +18 -7
- data/lib/space_architect/cli/architect.rb +123 -7
- data/lib/space_architect/cli/research.rb +3 -4
- data/lib/space_architect/harness.rb +73 -28
- data/lib/space_architect/session_sync/runner.rb +4 -2
- data/lib/space_architect/skill_installer.rb +3 -3
- data/lib/space_architect/templates/iteration.md.erb +35 -7
- data/lib/space_core/cli/help.rb +2 -0
- data/lib/space_core/commands.rb +32 -1
- data/lib/space_core/paths.rb +35 -0
- data/lib/space_core/space_store.rb +1 -1
- data/lib/space_core/version.rb +1 -1
- data/lib/space_src/cli/sync.rb +23 -10
- data/lib/space_src/cli.rb +1 -0
- data/lib/space_src/cloner.rb +2 -0
- data/lib/space_src/nav.rb +1 -0
- data/lib/space_src/sync/engine.rb +46 -7
- data/lib/space_src/sync/report.rb +18 -0
- data/lib/space_src.rb +1 -0
- data/skill/architect/SKILL.md +96 -14
- data/skill/architect/dispatch.md +27 -12
- metadata +2 -1
|
@@ -53,7 +53,7 @@ module Space::Core
|
|
|
53
53
|
def list
|
|
54
54
|
return [] unless spaces_dir.directory?
|
|
55
55
|
|
|
56
|
-
spaces_dir.
|
|
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
|
data/lib/space_core/version.rb
CHANGED
data/lib/space_src/cli/sync.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
|
data/lib/space_src/cloner.rb
CHANGED
|
@@ -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
|
-
# @
|
|
83
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
449
|
-
|
|
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"
|
data/skill/architect/SKILL.md
CHANGED
|
@@ -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
|
|
174
|
-
the
|
|
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
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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)
|
|
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 →
|
|
323
|
-
provision → write prompts → dispatch** — every lane gets a worktree;
|
|
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
|
data/skill/architect/dispatch.md
CHANGED
|
@@ -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.
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
(
|
|
282
|
-
|
|
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.
|
|
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.
|
|
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:
|
|
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
|