space-architect 1.1.0 → 1.3.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 +9 -0
- data/lib/space_architect/architect_mission.rb +345 -36
- data/lib/space_architect/cli/architect.rb +214 -2
- data/lib/space_architect/skill_installer.rb +107 -0
- data/lib/space_architect/templates/architect.md.erb +4 -0
- data/lib/space_architect/templates/brief.md.erb +43 -0
- data/lib/space_architect/templates/iteration.md.erb +26 -18
- data/lib/space_architect/terminal.rb +15 -0
- data/lib/space_architect/version.rb +1 -1
- data/lib/space_architect.rb +1 -0
- data/skill/architect/SKILL.md +329 -0
- data/skill/architect/dispatch.md +308 -0
- data/skill/architect/research.md +89 -0
- data/skill/architect-research/SKILL.md +165 -0
- data/skill/architect-research/lanes.md +191 -0
- data/skill/architect-vocabulary/SKILL.md +141 -0
- data/vendor/repo-tender/lib/space_architect/pristine/cli.rb +1 -10
- data/vendor/repo-tender/lib/space_architect/pristine.rb +7 -0
- metadata +9 -1
|
@@ -107,6 +107,12 @@ module SpaceArchitect
|
|
|
107
107
|
mission = ArchitectMission.new(space: sp)
|
|
108
108
|
sha = mission.freeze!(iteration)
|
|
109
109
|
terminal.say "Frozen #{iteration} at #{sha}"
|
|
110
|
+
ac = mission.acceptance_criteria(iteration)
|
|
111
|
+
unless ac.to_s.strip.empty?
|
|
112
|
+
terminal.say ""
|
|
113
|
+
terminal.say "Frozen Acceptance Criteria (quote these verbatim when judging):"
|
|
114
|
+
terminal.say ac
|
|
115
|
+
end
|
|
110
116
|
CLI.record_outcome(Outcome.new(exit_code: 0))
|
|
111
117
|
end
|
|
112
118
|
end
|
|
@@ -195,6 +201,201 @@ module SpaceArchitect
|
|
|
195
201
|
end
|
|
196
202
|
end
|
|
197
203
|
|
|
204
|
+
class Section < Dry::CLI::Command
|
|
205
|
+
include GlobalOptions
|
|
206
|
+
include Helpers
|
|
207
|
+
|
|
208
|
+
desc "Write a section of the iteration file and commit it (one call)"
|
|
209
|
+
argument :iteration, required: true, desc: "Iteration name"
|
|
210
|
+
argument :section, required: true, desc: "Section: grounds, specification, prompt, verdict"
|
|
211
|
+
argument :space, required: false, desc: "Space identifier (default: $PWD)"
|
|
212
|
+
option :from, default: nil, desc: "Read the section body from this file"
|
|
213
|
+
option :body, default: nil, desc: "Inline section body (one-liners)"
|
|
214
|
+
option :stdin, type: :boolean, default: false, desc: "Read the section body from stdin"
|
|
215
|
+
option :append, type: :boolean, default: false, desc: "Append a ### <lane> subsection instead of replacing"
|
|
216
|
+
option :lane, default: nil, desc: "Lane name for an appended ### subsection"
|
|
217
|
+
|
|
218
|
+
def call(iteration:, section:, space: nil, from: nil, body: nil, stdin: false, append: false, lane: nil, **opts)
|
|
219
|
+
setup_terminal(**opts.slice(:color, :colors))
|
|
220
|
+
handle_errors do
|
|
221
|
+
content = read_section_body(from: from, body: body, stdin: stdin)
|
|
222
|
+
render(store.find(space)) do |sp|
|
|
223
|
+
mission = ArchitectMission.new(space: sp)
|
|
224
|
+
res = mission.write_section!(iteration, section, body: content, append: append, lane: lane)
|
|
225
|
+
if res[:committed]
|
|
226
|
+
terminal.say "Committed #{res[:heading]} → #{res[:sha][0, 8]}"
|
|
227
|
+
terminal.say res[:diffstat] unless res[:diffstat].empty?
|
|
228
|
+
else
|
|
229
|
+
terminal.say "#{res[:heading]} written — no change to commit"
|
|
230
|
+
end
|
|
231
|
+
CLI.record_outcome(Outcome.new(exit_code: 0))
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
private
|
|
237
|
+
|
|
238
|
+
def read_section_body(from:, body:, stdin:)
|
|
239
|
+
return File.read(from) if from
|
|
240
|
+
return body if body
|
|
241
|
+
return $stdin.read if stdin
|
|
242
|
+
raise SpaceArchitect::Error, "provide the section body via --from <file>, --body <text>, or --stdin"
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
class Evidence < Dry::CLI::Command
|
|
247
|
+
include GlobalOptions
|
|
248
|
+
include Helpers
|
|
249
|
+
|
|
250
|
+
desc "Transcribe a lane's scratch report VERBATIM into Builder Report and commit"
|
|
251
|
+
argument :iteration, required: true, desc: "Iteration name"
|
|
252
|
+
argument :space, required: false, desc: "Space identifier (default: $PWD)"
|
|
253
|
+
option :lane, default: nil, desc: "Lane name (per-lane subsection; omit for a single-lane iteration)"
|
|
254
|
+
|
|
255
|
+
def call(iteration:, space: nil, lane: nil, **opts)
|
|
256
|
+
setup_terminal(**opts.slice(:color, :colors))
|
|
257
|
+
handle_errors do
|
|
258
|
+
render(store.find(space)) do |sp|
|
|
259
|
+
mission = ArchitectMission.new(space: sp)
|
|
260
|
+
res = mission.transcribe_evidence!(iteration, lane: lane)
|
|
261
|
+
terminal.say "Transcribed #{res[:lines]} lines → #{res[:sha][0, 8]}"
|
|
262
|
+
terminal.say "Builder STATUS: #{res[:status_line]}" if res[:status_line]
|
|
263
|
+
terminal.say "Now rule on the builder's PHASE 0 disagreements in the Verdict (a later session)."
|
|
264
|
+
CLI.record_outcome(Outcome.new(exit_code: 0))
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
class Merge < Dry::CLI::Command
|
|
271
|
+
include GlobalOptions
|
|
272
|
+
include Helpers
|
|
273
|
+
|
|
274
|
+
desc "Integrate ONE judged-passing lane (merges --no-ff; runs no gates, makes no verdict)"
|
|
275
|
+
argument :iteration, required: true, desc: "Iteration name"
|
|
276
|
+
argument :lane, required: true, desc: "Lane name (architect-judged passing)"
|
|
277
|
+
argument :space, required: false, desc: "Space identifier (default: $PWD)"
|
|
278
|
+
option :message, default: nil, desc: "Commit message for the lane's working-tree changes"
|
|
279
|
+
|
|
280
|
+
def call(iteration:, lane:, space: nil, message: nil, **opts)
|
|
281
|
+
setup_terminal(**opts.slice(:color, :colors))
|
|
282
|
+
handle_errors do
|
|
283
|
+
render(store.find(space)) do |sp|
|
|
284
|
+
mission = ArchitectMission.new(space: sp)
|
|
285
|
+
r = mission.merge_lane!(iteration, lane, message: message)
|
|
286
|
+
terminal.say "Merged #{lane} → #{r[:integration_branch]} (#{r[:merge_sha][0, 8]})"
|
|
287
|
+
terminal.say r[:diffstat] unless r[:diffstat].empty?
|
|
288
|
+
terminal.say "Gates NOT run — run `architect gate #{iteration}` against the integration branch."
|
|
289
|
+
CLI.record_outcome(Outcome.new(exit_code: 0))
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
class Integrate < Dry::CLI::Command
|
|
296
|
+
include GlobalOptions
|
|
297
|
+
include Helpers
|
|
298
|
+
|
|
299
|
+
desc "Integrate the architect-supplied set of passing lanes, in order (stops on conflict)"
|
|
300
|
+
argument :iteration, required: true, desc: "Iteration name"
|
|
301
|
+
argument :space, required: false, desc: "Space identifier (default: $PWD)"
|
|
302
|
+
option :lanes, required: true, desc: "Comma-separated passing lane names (you decide the set)"
|
|
303
|
+
option :teardown, type: :boolean, default: false, desc: "Remove worktrees + delete lane branches after merge"
|
|
304
|
+
|
|
305
|
+
def call(iteration:, space: nil, lanes:, teardown: false, **opts)
|
|
306
|
+
setup_terminal(**opts.slice(:color, :colors))
|
|
307
|
+
handle_errors do
|
|
308
|
+
render(store.find(space)) do |sp|
|
|
309
|
+
mission = ArchitectMission.new(space: sp)
|
|
310
|
+
lane_names = lanes.to_s.split(",").map(&:strip).reject(&:empty?)
|
|
311
|
+
results = mission.integrate!(iteration, lanes: lane_names, teardown: teardown)
|
|
312
|
+
results.each do |r|
|
|
313
|
+
terminal.say "Merged #{r[:lane]} → #{r[:integration_branch]} (#{r[:merge_sha][0, 8]})"
|
|
314
|
+
end
|
|
315
|
+
terminal.say "Gates NOT run — run `architect gate #{iteration}`; the verdict is the next session's."
|
|
316
|
+
CLI.record_outcome(Outcome.new(exit_code: 0))
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
class Gate < Dry::CLI::Command
|
|
323
|
+
include GlobalOptions
|
|
324
|
+
include Helpers
|
|
325
|
+
|
|
326
|
+
desc "Run the frozen Acceptance Criteria gate commands and stream raw output (no PASS/FAIL)"
|
|
327
|
+
argument :iteration, required: true, desc: "Iteration name"
|
|
328
|
+
argument :lane, required: false, desc: "Run in a lane worktree (default: the integration repo)"
|
|
329
|
+
argument :space, required: false, desc: "Space identifier (default: $PWD)"
|
|
330
|
+
|
|
331
|
+
def call(iteration:, lane: nil, space: nil, **opts)
|
|
332
|
+
setup_terminal(**opts.slice(:color, :colors))
|
|
333
|
+
handle_errors do
|
|
334
|
+
render(store.find(space)) do |sp|
|
|
335
|
+
mission = ArchitectMission.new(space: sp)
|
|
336
|
+
results = mission.run_gates(iteration, lane: lane)
|
|
337
|
+
results.each do |r|
|
|
338
|
+
terminal.say ""
|
|
339
|
+
terminal.say "── #{r[:ac].empty? ? "(gate)" : r[:ac]}: #{r[:command]} (exit #{r[:exit_code]})"
|
|
340
|
+
terminal.say r[:stdout].rstrip unless r[:stdout].strip.empty?
|
|
341
|
+
terminal.say r[:stderr].rstrip unless r[:stderr].strip.empty?
|
|
342
|
+
end
|
|
343
|
+
terminal.say ""
|
|
344
|
+
terminal.say "Raw gate output above — the PASS/FAIL/INVALID verdict is yours, read against the frozen thresholds."
|
|
345
|
+
CLI.record_outcome(Outcome.new(exit_code: 0))
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
class InstallSkills < Dry::CLI::Command
|
|
352
|
+
include GlobalOptions
|
|
353
|
+
include Helpers
|
|
354
|
+
|
|
355
|
+
desc "Install bundled skills (architect, architect-research, architect-vocabulary) for a harness"
|
|
356
|
+
option :provider, default: "claude", desc: "Harness: claude, codex, opencode, pi"
|
|
357
|
+
option :project, type: :boolean, default: false, desc: "Install to CWD instead of global"
|
|
358
|
+
option :force, type: :boolean, default: false, desc: "Overwrite existing skills that differ"
|
|
359
|
+
option :dry_run, type: :boolean, default: false, desc: "Print what would happen without writing files"
|
|
360
|
+
|
|
361
|
+
def call(provider: "claude", project: false, force: false, dry_run: false, **opts)
|
|
362
|
+
setup_terminal(**opts.slice(:color, :colors))
|
|
363
|
+
handle_errors do
|
|
364
|
+
result = SkillInstaller.install(provider, project: project, force: force,
|
|
365
|
+
env: project_config.env, dry_run: dry_run)
|
|
366
|
+
verb = dry_run ? "Would install" : "Installed"
|
|
367
|
+
terminal.say "#{verb} skills for #{provider} → #{terminal.path(result[:dest_root])}"
|
|
368
|
+
result[:skills].each do |s|
|
|
369
|
+
terminal.say " #{s[:name]}: #{terminal.style_skill_action(s[:action])} (#{terminal.path(s[:path])})"
|
|
370
|
+
end
|
|
371
|
+
CLI.record_outcome(Outcome.new(exit_code: 0))
|
|
372
|
+
end
|
|
373
|
+
end
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
module Brief
|
|
377
|
+
class New < Dry::CLI::Command
|
|
378
|
+
include GlobalOptions
|
|
379
|
+
include Helpers
|
|
380
|
+
|
|
381
|
+
desc "Scaffold the durable mission brief (architecture/BRIEF.md)"
|
|
382
|
+
argument :space, required: false, desc: "Space identifier (default: $PWD)"
|
|
383
|
+
option :force, type: :boolean, default: false, desc: "Overwrite an existing BRIEF.md"
|
|
384
|
+
|
|
385
|
+
def call(space: nil, force: false, **opts)
|
|
386
|
+
setup_terminal(**opts.slice(:color, :colors))
|
|
387
|
+
handle_errors do
|
|
388
|
+
render(store.find(space)) do |sp|
|
|
389
|
+
mission = ArchitectMission.new(space: sp)
|
|
390
|
+
path = mission.brief_new!(force: force)
|
|
391
|
+
terminal.say "Brief ready: #{terminal.path(path)}"
|
|
392
|
+
CLI.record_outcome(Outcome.new(exit_code: 0))
|
|
393
|
+
end
|
|
394
|
+
end
|
|
395
|
+
end
|
|
396
|
+
end
|
|
397
|
+
end
|
|
398
|
+
|
|
198
399
|
module Worktree
|
|
199
400
|
class Add < Dry::CLI::Command
|
|
200
401
|
include GlobalOptions
|
|
@@ -208,14 +409,16 @@ module SpaceArchitect
|
|
|
208
409
|
option :harness, default: "claude-code", desc: "Harness (claude-code, opencode)"
|
|
209
410
|
option :model, default: nil, desc: "Model (required for opencode)"
|
|
210
411
|
option :effort, default: nil, desc: "Reasoning effort (opencode only; sets reasoningEffort in the model config)"
|
|
412
|
+
option :touch, default: nil, desc: "Comma-separated file globs the lane may touch (records its touch_set for in-bounds + merge checks)"
|
|
211
413
|
|
|
212
|
-
def call(repo:, iteration:, lane:, base: nil, harness: "claude-code", model: nil, effort: nil, **opts)
|
|
414
|
+
def call(repo:, iteration:, lane:, base: nil, harness: "claude-code", model: nil, effort: nil, touch: nil, **opts)
|
|
213
415
|
setup_terminal(**opts.slice(:color, :colors))
|
|
214
416
|
handle_errors do
|
|
215
417
|
render(store.find) do |sp|
|
|
216
418
|
mission = ArchitectMission.new(space: sp)
|
|
419
|
+
touch_set = touch ? touch.split(",").map(&:strip).reject(&:empty?) : nil
|
|
217
420
|
result = mission.worktree_add(repo, iteration, lane, base: base,
|
|
218
|
-
harness: harness, model: model, effort: effort)
|
|
421
|
+
harness: harness, model: model, effort: effort, touch: touch_set)
|
|
219
422
|
terminal.say "Worktree: #{terminal.path(result[:worktree])}"
|
|
220
423
|
terminal.say "Base SHA: #{result[:base_sha]}"
|
|
221
424
|
CLI.record_outcome(Outcome.new(exit_code: 0))
|
|
@@ -376,6 +579,15 @@ SpaceArchitect::CLI::Registry.register "status", SpaceArchitect::CLI::Architect:
|
|
|
376
579
|
SpaceArchitect::CLI::Registry.register "freeze", SpaceArchitect::CLI::Architect::Freeze
|
|
377
580
|
SpaceArchitect::CLI::Registry.register "verify", SpaceArchitect::CLI::Architect::Verify
|
|
378
581
|
SpaceArchitect::CLI::Registry.register "dispatch", SpaceArchitect::CLI::Architect::Dispatch
|
|
582
|
+
SpaceArchitect::CLI::Registry.register "section", SpaceArchitect::CLI::Architect::Section
|
|
583
|
+
SpaceArchitect::CLI::Registry.register "evidence", SpaceArchitect::CLI::Architect::Evidence
|
|
584
|
+
SpaceArchitect::CLI::Registry.register "merge", SpaceArchitect::CLI::Architect::Merge
|
|
585
|
+
SpaceArchitect::CLI::Registry.register "integrate", SpaceArchitect::CLI::Architect::Integrate
|
|
586
|
+
SpaceArchitect::CLI::Registry.register "gate", SpaceArchitect::CLI::Architect::Gate
|
|
587
|
+
SpaceArchitect::CLI::Registry.register "install-skills", SpaceArchitect::CLI::Architect::InstallSkills
|
|
588
|
+
SpaceArchitect::CLI::Registry.register "brief" do |b|
|
|
589
|
+
b.register "new", SpaceArchitect::CLI::Architect::Brief::New
|
|
590
|
+
end
|
|
379
591
|
SpaceArchitect::CLI::Registry.register "worktree" do |wt|
|
|
380
592
|
wt.register "add", SpaceArchitect::CLI::Architect::Worktree::Add
|
|
381
593
|
wt.register "remove", SpaceArchitect::CLI::Architect::Worktree::Remove
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "pathname"
|
|
5
|
+
|
|
6
|
+
module SpaceArchitect
|
|
7
|
+
module SkillInstaller
|
|
8
|
+
PROVIDERS = %w[claude codex opencode pi].freeze
|
|
9
|
+
|
|
10
|
+
class << self
|
|
11
|
+
def source_root
|
|
12
|
+
Pathname.new(__dir__).parent.parent.join("skill")
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def dest_root(provider, project:, env:, cwd: Dir.pwd)
|
|
16
|
+
case provider.to_s
|
|
17
|
+
when "claude"
|
|
18
|
+
base = project ? Pathname.new(cwd) : Pathname.new(XDG.home(env: env))
|
|
19
|
+
base.join(".claude", "skills")
|
|
20
|
+
when "codex"
|
|
21
|
+
base = project ? Pathname.new(cwd) : Pathname.new(XDG.home(env: env))
|
|
22
|
+
base.join(".agents", "skills")
|
|
23
|
+
when "opencode"
|
|
24
|
+
project ? Pathname.new(cwd).join(".opencode", "skills") : XDG.config_home(env: env).join("skills")
|
|
25
|
+
when "pi"
|
|
26
|
+
base = project ? Pathname.new(cwd) : Pathname.new(pi_agent_dir(env: env))
|
|
27
|
+
base.join("skills")
|
|
28
|
+
else
|
|
29
|
+
raise Error, "Unknown provider '#{provider}'. Expected one of: #{PROVIDERS.join(', ')}"
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def install(provider, project:, force:, env:, cwd: Dir.pwd, dry_run: false)
|
|
34
|
+
validate_provider!(provider)
|
|
35
|
+
dest = dest_root(provider, project: project, env: env, cwd: cwd)
|
|
36
|
+
results = []
|
|
37
|
+
|
|
38
|
+
source_skills.each do |skill_dir|
|
|
39
|
+
name = skill_dir.basename.to_s
|
|
40
|
+
skill_dest = dest.join(name)
|
|
41
|
+
results << install_skill(skill_dir, skill_dest, force: force, dry_run: dry_run)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
{ dest_root: dest, skills: results, dry_run: dry_run }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def source_skills
|
|
48
|
+
source_root.children.select(&:directory?)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def validate_provider!(provider)
|
|
54
|
+
return if PROVIDERS.include?(provider.to_s)
|
|
55
|
+
|
|
56
|
+
raise Error, "Unknown provider '#{provider}'. Expected one of: #{PROVIDERS.join(', ')}"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def pi_agent_dir(env:)
|
|
60
|
+
Pathname.new(env.fetch("PI_CODING_AGENT_DIR", File.join(XDG.home(env: env), ".pi", "agent")))
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def install_skill(source, dest, force:, dry_run:)
|
|
64
|
+
name = source.basename.to_s
|
|
65
|
+
|
|
66
|
+
if dest.exist?
|
|
67
|
+
if same_content?(source, dest)
|
|
68
|
+
return { name: name, action: :unchanged, path: dest }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
unless force
|
|
72
|
+
return { name: name, action: :conflict, path: dest } if dry_run
|
|
73
|
+
|
|
74
|
+
raise Error,
|
|
75
|
+
"Refusing to overwrite existing skill at #{dest}. Re-run with --force."
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
unless dry_run
|
|
79
|
+
FileUtils.rm_rf(dest)
|
|
80
|
+
FileUtils.cp_r(source, dest)
|
|
81
|
+
end
|
|
82
|
+
{ name: name, action: dry_run ? :would_update : :updated, path: dest }
|
|
83
|
+
else
|
|
84
|
+
unless dry_run
|
|
85
|
+
FileUtils.mkdir_p(dest.parent)
|
|
86
|
+
FileUtils.cp_r(source, dest)
|
|
87
|
+
end
|
|
88
|
+
{ name: name, action: dry_run ? :would_install : :installed, path: dest }
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def same_content?(source, dest)
|
|
93
|
+
return false unless dest.directory?
|
|
94
|
+
|
|
95
|
+
source_files = Dir.glob("#{source}/**/*").reject { |f| File.directory?(f) }
|
|
96
|
+
dest_files = Dir.glob("#{dest}/**/*").reject { |f| File.directory?(f) }
|
|
97
|
+
|
|
98
|
+
return false if source_files.length != dest_files.length
|
|
99
|
+
|
|
100
|
+
source_files.sort.zip(dest_files.sort).all? do |sf, df|
|
|
101
|
+
rel = sf.sub("#{source}/", "")
|
|
102
|
+
df.end_with?(rel) && File.read(sf) == File.read(df)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
> architecture/I<NN>-<iteration>.md — this file only indexes the iterations and carries
|
|
5
5
|
> mission-wide state. Keep it short (~150 lines): the next session must grok it
|
|
6
6
|
> in under a minute. Not in the committed architecture = didn't happen.
|
|
7
|
+
>
|
|
8
|
+
> Durable mission contract: `architecture/BRIEF.md` (numbered §sections, cited as BRIEF §N).
|
|
9
|
+
> Create it with `architect brief new`. Edits to a §section are mission-scope decisions —
|
|
10
|
+
> log them in the Decisions log below, never as silent per-iteration drift.
|
|
7
11
|
|
|
8
12
|
## TL;DR (keep current)
|
|
9
13
|
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# BRIEF — <%= @_title %>
|
|
2
|
+
|
|
3
|
+
> Durable mission contract for the Architect Loop. The numbered §sections below are the
|
|
4
|
+
> stable, cross-iteration address space: every iteration cites them as **BRIEF §N** in its
|
|
5
|
+
> Grounds, Specification, Acceptance Criteria, and Verdict (e.g. `(BRIEF §3.1)`), the way each
|
|
6
|
+
> gate addresses its intent back to one frozen reference. Frozen early; edits are mission-scope
|
|
7
|
+
> decisions logged in ARCHITECT.md's Decisions log, never silent per-iteration drift. Optional —
|
|
8
|
+
> a discovery mission may defer this and cite per-iteration Grounds until the shape stabilizes,
|
|
9
|
+
> then promote the consolidated picture here once.
|
|
10
|
+
|
|
11
|
+
## 1. Goal & non-goals
|
|
12
|
+
|
|
13
|
+
<!-- One paragraph of goal; an explicit non-goals list. §1 is the cardinal-intent address —
|
|
14
|
+
iterations and verdicts cite it for the invariants that must never break. -->
|
|
15
|
+
|
|
16
|
+
## 2. Constraints / frozen stack
|
|
17
|
+
|
|
18
|
+
<!-- Languages, gems/deps and pinned versions, platform constraints. "No new dependencies"
|
|
19
|
+
boundaries in iteration specs cite BRIEF §2. -->
|
|
20
|
+
|
|
21
|
+
## 3. Domain model
|
|
22
|
+
|
|
23
|
+
<!-- The nouns and their relationships; the data/contract shapes that span iterations. -->
|
|
24
|
+
|
|
25
|
+
### 3.1
|
|
26
|
+
|
|
27
|
+
## 4. Layout
|
|
28
|
+
|
|
29
|
+
<!-- Where things live: directories, entry points, the seams iterations build against. -->
|
|
30
|
+
|
|
31
|
+
## 5. Iteration map
|
|
32
|
+
|
|
33
|
+
<!-- The planned iterations, each independently shippable; the Acceptance Criteria are the
|
|
34
|
+
frozen proof. An iteration's Specification cites its row here (e.g. "BRIEF §5 Slice 3"). -->
|
|
35
|
+
|
|
36
|
+
## 6. Cross-iteration risks / PHASE-0 challenge list
|
|
37
|
+
|
|
38
|
+
<!-- Known risks and the things a builder's PHASE 0 should challenge — cited from specs as
|
|
39
|
+
"BRIEF §6". -->
|
|
40
|
+
|
|
41
|
+
## 7. Definition of done (whole mission)
|
|
42
|
+
|
|
43
|
+
<!-- The mission-level DoD. Iteration gates diff against "BRIEF §7 DoD". -->
|
|
@@ -9,21 +9,23 @@
|
|
|
9
9
|
|
|
10
10
|
## Grounds
|
|
11
11
|
|
|
12
|
-
<!-- WHY. Research/
|
|
13
|
-
non-goals, verified facts WITH citation URLs, open questions.
|
|
14
|
-
|
|
12
|
+
<!-- WHY. Research/spec distilled: problem, decision + why, requirements,
|
|
13
|
+
non-goals, verified facts WITH citation URLs, open questions. If
|
|
14
|
+
architecture/BRIEF.md exists, cite **BRIEF §N** instead of restating — shrink this
|
|
15
|
+
to deltas + citations. Optional — delete this section for iterations that needed
|
|
16
|
+
no research. Write + commit: `architect section <%= @_name %> grounds --from <file>`. -->
|
|
15
17
|
|
|
16
18
|
## Specification
|
|
17
19
|
|
|
18
20
|
<!-- WHAT / HOW — the full, self-contained delegation contract.
|
|
19
|
-
|
|
21
|
+
Write + commit: `architect section <%= @_name %> specification --from <file>`. -->
|
|
20
22
|
|
|
21
|
-
- **Objective** — what to build and why
|
|
23
|
+
- **Objective** — what to build and why; cite **BRIEF §N** for durable context (e.g. `(BRIEF §3.1)`).
|
|
22
24
|
- **Output format** — raw tables, numbers, commit SHAs, test output paths.
|
|
23
25
|
- **Tool guidance** — exact verification commands for the target repo; the
|
|
24
26
|
APIs/formats/versions to verify against live dependencies before writing code.
|
|
25
27
|
- **Boundaries** — may-touch / must-not-touch / out-of-scope; no placeholders;
|
|
26
|
-
no refactors beyond the task.
|
|
28
|
+
no refactors beyond the task. (Frozen stack: cite `BRIEF §2`.)
|
|
27
29
|
- **Lane plan** — 1–4 lanes, each declaring: target repo `repos/<repo>`,
|
|
28
30
|
file-touch set (overlap-checked), objective, output format, boundaries.
|
|
29
31
|
- **Effort** — `think hard` … `ultrathink` per lane, with one line of why.
|
|
@@ -34,33 +36,39 @@ Commit: "<%= @_ordinal %>: specification". -->
|
|
|
34
36
|
commits this file and records its SHA as freeze_sha. Read-only afterward — any
|
|
35
37
|
change to Grounds/Specification/Acceptance Criteria = automatic iteration FAIL. -->
|
|
36
38
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
39
|
+
> Gate-pass is necessary, not sufficient: the architect also reads the diff against the
|
|
40
|
+
> cited **BRIEF §sections** intent and the §1 cardinal invariant before the verdict.
|
|
41
|
+
|
|
42
|
+
| AC# | Command | Threshold | Brief § |
|
|
43
|
+
|-----|---------|-----------|---------|
|
|
44
|
+
| | | | |
|
|
40
45
|
|
|
41
46
|
## Builder Prompt
|
|
42
47
|
|
|
43
48
|
<!-- The exact lane-prompt(s) dispatched, recorded as provenance. One ###
|
|
44
49
|
subsection per lane. A copy is written to build/<%= @_ordinal %>-<%= @_name %>-<lane>/prompt.md
|
|
45
|
-
for stdin dispatch.
|
|
50
|
+
for stdin dispatch. Record + commit:
|
|
51
|
+
`architect section <%= @_name %> prompt --append --lane <lane> --from build/<%= @_ordinal %>-<%= @_name %>-<lane>/prompt.md`. -->
|
|
46
52
|
|
|
47
53
|
## Builder Report
|
|
48
54
|
|
|
49
55
|
<!-- RAW EVIDENCE ONLY — tables, numbers, command output. The builder writes this
|
|
50
56
|
to build/<%= @_ordinal %>-<%= @_name %>-<lane>/report.md; the architect transcribes it here
|
|
51
|
-
VERBATIM
|
|
52
|
-
|
|
57
|
+
VERBATIM with `architect evidence <%= @_name %> --lane <lane>` (byte-for-byte, no
|
|
58
|
+
interpretation). One ### subsection per lane; includes the builder's PHASE 0
|
|
59
|
+
disagreements and its STATUS line. -->
|
|
53
60
|
|
|
54
61
|
## Verdict
|
|
55
62
|
|
|
56
63
|
<!-- ARCHITECT JUDGMENT, written in a LATER session than the dispatch.
|
|
57
|
-
|
|
64
|
+
Write + commit: `architect section <%= @_name %> verdict --from <file>`. -->
|
|
58
65
|
|
|
59
|
-
- **Disagreement rulings** — each PHASE 0 disagreement: ACCEPT / REJECT / MODIFY + why
|
|
66
|
+
- **Disagreement rulings** — each PHASE 0 disagreement: ACCEPT / REJECT / MODIFY + why; cite the
|
|
67
|
+
BRIEF §section in tension beside file:line.
|
|
60
68
|
- **Acceptance Criteria integrity** — frozen sections unchanged since freeze (`architect verify <%= @_name %>`)?
|
|
61
69
|
|
|
62
|
-
| AC# | Raw result | Verdict |
|
|
63
|
-
|
|
64
|
-
| | | PASS/FAIL/INVALID |
|
|
70
|
+
| AC# | Raw result | Brief § | Verdict |
|
|
71
|
+
|-----|------------|---------|---------|
|
|
72
|
+
| | | | PASS/FAIL/INVALID |
|
|
65
73
|
|
|
66
|
-
- **Iteration** — KILL / CONTINUE + the single decisive reason.
|
|
74
|
+
- **Iteration** — KILL / CONTINUE + the single decisive reason (e.g. "diff vs BRIEF §1/§3.3 faithful — CONTINUE").
|
|
@@ -74,6 +74,21 @@ module SpaceArchitect
|
|
|
74
74
|
end.wait
|
|
75
75
|
end
|
|
76
76
|
|
|
77
|
+
def style_skill_action(action)
|
|
78
|
+
case action.to_s
|
|
79
|
+
when "installed", "updated"
|
|
80
|
+
pastel.green(action)
|
|
81
|
+
when "would_install", "would_update"
|
|
82
|
+
pastel.cyan(action)
|
|
83
|
+
when "unchanged"
|
|
84
|
+
pastel.bright_black(action)
|
|
85
|
+
when "conflict"
|
|
86
|
+
pastel.yellow(action)
|
|
87
|
+
else
|
|
88
|
+
action.to_s
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
77
92
|
private
|
|
78
93
|
|
|
79
94
|
def color_mode
|
data/lib/space_architect.rb
CHANGED
|
@@ -19,6 +19,7 @@ require_relative "space_architect/git_client"
|
|
|
19
19
|
require_relative "space_architect/mise_client"
|
|
20
20
|
require_relative "space_architect/space_store"
|
|
21
21
|
require_relative "space_architect/shell_integration"
|
|
22
|
+
require_relative "space_architect/skill_installer"
|
|
22
23
|
require_relative "space_architect/terminal"
|
|
23
24
|
require_relative "space_architect/harness"
|
|
24
25
|
require_relative "space_architect/dispatcher"
|